diff --git a/docs/src/main/resources/documentation/architecture/rollout-management.md b/docs/src/main/resources/documentation/architecture/rollout-management.md
index 3abbadd65..d55fa493f 100644
--- a/docs/src/main/resources/documentation/architecture/rollout-management.md
+++ b/docs/src/main/resources/documentation/architecture/rollout-management.md
@@ -24,7 +24,12 @@ The following capabilities are currently supported by the _Rollout Management_:
- Selection of targets as input for the rollout based on _target filter_ functionality.
- Selection of a _DistributionSet_.
- Auto-splitting of the input target list into a defined number deployment groups.
-
+- Approval workflow
+ - Has to be enabled explicitly in configuration.
+ - Enables a workflow that requires a user with adequate permissions to review any new or updated rollout before it
+ can be started.
+ - Allows integration with 3rd party workflow engines.
+
- Cascading start of the deployment groups based on installation status of the previous group.
- Emergency shutdown of the rollout in case a group exceeds the defined error threshold.
- Rollout progress monitoring for the entire rollout and the individual groups.
diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java
index 1c995b262..d51f61349 100644
--- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java
+++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java
@@ -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
diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Rollout.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Rollout.java
index 3e6cd314b..586e53513 100644
--- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Rollout.java
+++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Rollout.java
@@ -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 true 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
+ }
+
}
diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/TenantConfigurationProperties.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/TenantConfigurationProperties.java
index 5fbeb070d..626fda3db 100644
--- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/TenantConfigurationProperties.java
+++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/TenantConfigurationProperties.java
@@ -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.
diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/AbstractRolloutManagement.java b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/AbstractRolloutManagement.java
index 742be2a70..201f9b4b9 100644
--- a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/AbstractRolloutManagement.java
+++ b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/AbstractRolloutManagement.java
@@ -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 action) {
diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/RolloutApprovalStrategy.java b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/RolloutApprovalStrategy.java
new file mode 100644
index 000000000..9008db19a
--- /dev/null
+++ b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/RolloutApprovalStrategy.java
@@ -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);
+}
diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/resources/hawkbit-repository-defaults.properties b/hawkbit-repository/hawkbit-repository-core/src/main/resources/hawkbit-repository-defaults.properties
index aca52648b..553501886 100644
--- a/hawkbit-repository/hawkbit-repository-core/src/main/resources/hawkbit-repository-defaults.properties
+++ b/hawkbit-repository/hawkbit-repository-core/src/main/resources/hawkbit-repository-defaults.properties
@@ -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
diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/DefaultRolloutApprovalStrategy.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/DefaultRolloutApprovalStrategy.java
new file mode 100644
index 000000000..6fe288128
--- /dev/null
+++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/DefaultRolloutApprovalStrategy.java
@@ -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();
+ }
+}
diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java
index 6cd0921ff..05b2e7092 100644
--- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java
+++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java
@@ -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
diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java
index afdb944b8..b4e4d221b 100644
--- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java
+++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java
@@ -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);
}
/**
diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaRollout.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaRollout.java
index 4941ebf7a..64540421e 100644
--- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaRollout.java
+++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaRollout.java
@@ -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;
+ }
+
}
diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_7__add_rollout_approval_fields___DB2.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_7__add_rollout_approval_fields___DB2.sql
new file mode 100644
index 000000000..8438d909e
--- /dev/null
+++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_7__add_rollout_approval_fields___DB2.sql
@@ -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);
\ No newline at end of file
diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_7__add_rollout_approval_fields___H2.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_7__add_rollout_approval_fields___H2.sql
new file mode 100644
index 000000000..8438d909e
--- /dev/null
+++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_7__add_rollout_approval_fields___H2.sql
@@ -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);
\ No newline at end of file
diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_7__add_rollout_approval_fields___MYSQL.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_7__add_rollout_approval_fields___MYSQL.sql
new file mode 100644
index 000000000..8438d909e
--- /dev/null
+++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_7__add_rollout_approval_fields___MYSQL.sql
@@ -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);
\ No newline at end of file
diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_7__add_rollout_approval_fields___SQL_SERVER.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_7__add_rollout_approval_fields___SQL_SERVER.sql
new file mode 100644
index 000000000..8438d909e
--- /dev/null
+++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_7__add_rollout_approval_fields___SQL_SERVER.sql
@@ -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);
\ No newline at end of file
diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/AbstractJpaIntegrationTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/AbstractJpaIntegrationTest.java
index 98a2d6861..baef4ace8 100644
--- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/AbstractJpaIntegrationTest.java
+++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/AbstractJpaIntegrationTest.java
@@ -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 findActionsByRolloutAndStatus(final Rollout rollout, final Action.Status actionStatus) {
return Lists.newArrayList(actionRepository.findByRolloutIdAndStatus(PAGE, rollout.getId(), actionStatus));
diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java
index 33ba52992..50fe67e9a 100644
--- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java
+++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java
@@ -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),
diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/TestConfiguration.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/TestConfiguration.java
index 5fdfa4b44..0f5885c4d 100644
--- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/TestConfiguration.java
+++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/TestConfiguration.java
@@ -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
diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/RolloutTestApprovalStrategy.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/RolloutTestApprovalStrategy.java
new file mode 100644
index 000000000..e065c4d43
--- /dev/null
+++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/RolloutTestApprovalStrategy.java
@@ -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;
+ }
+}
diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java
index a026078d1..4b8a3585c 100644
--- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java
+++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java
@@ -81,6 +81,36 @@ public interface MgmtRolloutRestApi {
MediaType.APPLICATION_JSON_VALUE })
ResponseEntity create(MgmtRolloutRestRequestBody rolloutRequestBody);
+ /**
+ * Handles the request for approving a rollout.
+ *
+ * @param rolloutId
+ * the ID of the rollout to be approved.
+ * @param remark
+ * an optional remark on the approval decision
+ * @return OK response (200) if rollout is approved now. In case of any
+ * exception the corresponding errors occur.
+ */
+ @RequestMapping(method = RequestMethod.POST, value = "/{rolloutId}/approve", produces = { MediaTypes.HAL_JSON_VALUE,
+ MediaType.APPLICATION_JSON_VALUE })
+ ResponseEntity approve(@PathVariable("rolloutId") Long rolloutId,
+ @RequestParam(value = "remark", required = false) String remark);
+
+ /**
+ * Handles the request for denying the approval of a rollout.
+ *
+ * @param rolloutId
+ * the ID of the rollout to be denied.
+ * @param remark
+ * an optional remark on the denial decision
+ * @return OK response (200) if rollout is denied now. In case of any
+ * exception the corresponding errors occur.
+ */
+ @RequestMapping(method = RequestMethod.POST, value = "/{rolloutId}/deny", produces = { MediaTypes.HAL_JSON_VALUE,
+ MediaType.APPLICATION_JSON_VALUE })
+ ResponseEntity deny(@PathVariable("rolloutId") Long rolloutId,
+ @RequestParam(value = "remark", required = false) String remark);
+
/**
* Handles the POST request for starting a rollout.
*
diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java
index 478792add..87638e58f 100644
--- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java
+++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java
@@ -88,6 +88,8 @@ final class MgmtRolloutMapper {
body.add(linkTo(methodOn(MgmtRolloutRestApi.class).start(rollout.getId())).withRel("start"));
body.add(linkTo(methodOn(MgmtRolloutRestApi.class).pause(rollout.getId())).withRel("pause"));
body.add(linkTo(methodOn(MgmtRolloutRestApi.class).resume(rollout.getId())).withRel("resume"));
+ body.add(linkTo(methodOn(MgmtRolloutRestApi.class).approve(rollout.getId(), null)).withRel("approve"));
+ body.add(linkTo(methodOn(MgmtRolloutRestApi.class).deny(rollout.getId(), null)).withRel("deny"));
body.add(linkTo(methodOn(MgmtRolloutRestApi.class).getRolloutGroups(rollout.getId(),
MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET_VALUE,
MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT_VALUE, null, null)).withRel("groups"));
diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java
index cb40208fd..3f9f94480 100644
--- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java
+++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java
@@ -129,6 +129,18 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi {
return ResponseEntity.status(HttpStatus.CREATED).body(MgmtRolloutMapper.toResponseRollout(rollout, true));
}
+ @Override
+ public ResponseEntity approve(@PathVariable("rolloutId") final Long rolloutId, final String remark) {
+ rolloutManagement.approveOrDeny(rolloutId, Rollout.ApprovalDecision.APPROVED, remark);
+ return ResponseEntity.ok().build();
+ }
+
+ @Override
+ public ResponseEntity deny(@PathVariable("rolloutId") final Long rolloutId, final String remark) {
+ rolloutManagement.approveOrDeny(rolloutId, Rollout.ApprovalDecision.DENIED, remark);
+ return ResponseEntity.ok().build();
+ }
+
@Override
public ResponseEntity start(@PathVariable("rolloutId") final Long rolloutId) {
this.rolloutManagement.start(rolloutId);
diff --git a/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/rollout-api-guide.adoc b/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/rollout-api-guide.adoc
index ad6b2962d..c3137bef5 100644
--- a/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/rollout-api-guide.adoc
+++ b/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/rollout-api-guide.adoc
@@ -179,6 +179,81 @@ include::../errors/415.adoc[]
include::../errors/429.adoc[]
|===
+== POST /rest/v1/rollouts/{rolloutId}/approve
+
+=== Implementation Notes
+Handles the POST request of approving a created rollout within SP.
+Only possible if approval workflow is enabled in system configuration and rollout is in state WAITING_FOR_APPROVAL.
+Required Permission: APPROVE_ROLLOUT
+
+=== Approve Rollout
+
+==== CURL
+
+include::{snippets}/rollouts/approve-rollout/curl-request.adoc[]
+
+
+==== Request URL
+
+include::{snippets}/rollouts/approve-rollout/http-request.adoc[]
+
+
+=== Response (Status 200)
+
+==== Response example
+
+include::{snippets}/rollouts/approve-rollout/http-response.adoc[]
+
+=== Error responses
+
+|===
+| HTTP Status Code | Reason | Response Model
+include::../errors/400.adoc[]
+include::../errors/401.adoc[]
+include::../errors/403.adoc[]
+include::../errors/405.adoc[]
+include::../errors/406.adoc[]
+include::../errors/429.adoc[]
+|===
+
+== POST /rest/v1/rollouts/{rolloutId}/deny
+
+=== Implementation Notes
+Handles the POST request of denying a created rollout within SP.
+Only possible if approval workflow is enabled in system configuration and rollout is in state WAITING_FOR_APPROVAL.
+Required Permission: APPROVE_ROLLOUT
+
+=== Deny Rollout
+
+
+==== CURL
+
+include::{snippets}/rollouts/deny-rollout/curl-request.adoc[]
+
+
+==== Request URL
+
+include::{snippets}/rollouts/deny-rollout/http-request.adoc[]
+
+
+=== Response (Status 200)
+
+==== Response example
+
+include::{snippets}/rollouts/deny-rollout/http-response.adoc[]
+
+=== Error responses
+
+|===
+| HTTP Status Code | Reason | Response Model
+include::../errors/400.adoc[]
+include::../errors/401.adoc[]
+include::../errors/403.adoc[]
+include::../errors/405.adoc[]
+include::../errors/406.adoc[]
+include::../errors/429.adoc[]
+|===
+
== POST /rest/v1/rollouts/{rolloutId}/start
=== Implementation Notes
diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java
index bee86013a..82dbac86b 100644
--- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java
+++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java
@@ -103,6 +103,8 @@ public final class MgmtApiModelProperties {
public static final String ROLLOUT_LINKS_START_ASYNC = "Link to start the rollout in async mode";
public static final String ROLLOUT_LINKS_PAUSE = "Link to pause a running rollout";
public static final String ROLLOUT_LINKS_RESUME = "Link to resume a paused rollout";
+ public static final String ROLLOUT_LINKS_APPROVE = "Link to approve a rollout";
+ public static final String ROLLOUT_LINKS_DENY = "Link to deny a rollout";
public static final String ROLLOUT_LINKS_GROUPS = "Link to retrieve the groups a rollout";
public static final String ROLLOUT_START_ASYNC = "Start the rollout asynchronous";
diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/RolloutResourceDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/RolloutResourceDocumentationTest.java
index 91b2abadd..4e771effa 100644
--- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/RolloutResourceDocumentationTest.java
+++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/RolloutResourceDocumentationTest.java
@@ -33,6 +33,7 @@ import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupSuccessActi
import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupSuccessCondition;
import org.eclipse.hawkbit.repository.model.RolloutGroupConditionBuilder;
import org.eclipse.hawkbit.repository.model.RolloutGroupConditions;
+import org.eclipse.hawkbit.repository.test.util.RolloutTestApprovalStrategy;
import org.eclipse.hawkbit.rest.documentation.AbstractApiRestDocumentation;
import org.eclipse.hawkbit.rest.documentation.ApiModelPropertiesGeneric;
import org.eclipse.hawkbit.rest.documentation.DocumenationResponseFieldsSnippet;
@@ -41,6 +42,7 @@ import org.eclipse.hawkbit.rest.util.JsonBuilder;
import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter;
import org.junit.Before;
import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.MediaType;
@@ -62,12 +64,16 @@ import ru.yandex.qatools.allure.annotations.Stories;
@Stories("Rollout Resource")
public class RolloutResourceDocumentationTest extends AbstractApiRestDocumentation {
+ @Autowired
+ private RolloutTestApprovalStrategy approvalStrategy;
+
@Override
@Before
public void setUp() {
this.resourceName = "rollouts";
super.setUp();
arrayPrefix = "content[].";
+ approvalStrategy.setApprovalNeeded(false);
}
@Test
@@ -127,7 +133,6 @@ public class RolloutResourceDocumentationTest extends AbstractApiRestDocumentati
allFieldDescriptor.add(
fieldWithPath(arrayPrefix + "totalTargets").description(MgmtApiModelProperties.ROLLOUT_TOTAL_TARGETS));
allFieldDescriptor.add(fieldWithPath(arrayPrefix + "_links.self").ignored());
-
if (withDetails) {
allFieldDescriptor.add(fieldWithPath(arrayPrefix + "totalTargetsPerStatus")
.description(MgmtApiModelProperties.ROLLOUT_TOTAL_TARGETS_PER_STATUS));
@@ -139,6 +144,10 @@ public class RolloutResourceDocumentationTest extends AbstractApiRestDocumentati
.description(MgmtApiModelProperties.ROLLOUT_LINKS_RESUME));
allFieldDescriptor.add(fieldWithPath(arrayPrefix + "_links.groups")
.description(MgmtApiModelProperties.ROLLOUT_LINKS_GROUPS));
+ allFieldDescriptor.add(fieldWithPath(arrayPrefix + "_links.approve")
+ .description(MgmtApiModelProperties.ROLLOUT_LINKS_APPROVE));
+ allFieldDescriptor.add(fieldWithPath(arrayPrefix + "_links.deny")
+ .description(MgmtApiModelProperties.ROLLOUT_LINKS_DENY));
}
return new DocumenationResponseFieldsSnippet(allFieldDescriptor);
@@ -386,6 +395,29 @@ public class RolloutResourceDocumentationTest extends AbstractApiRestDocumentati
pathParameters(parameterWithName("rolloutId").description(ApiModelPropertiesGeneric.ITEM_ID))));
}
+ @Test
+ @Description("Handles the POST request of approving a rollout. Required Permission: "
+ + SpPermission.APPROVE_ROLLOUT)
+ public void approveRollout() throws Exception {
+ approvalStrategy.setApprovalNeeded(true);
+ final Rollout rollout = createRolloutEntity();
+ mockMvc.perform(post(MgmtRestConstants.ROLLOUT_V1_REQUEST_MAPPING + "/{rolloutId}/approve", rollout.getId())
+ .accept(MediaTypes.HAL_JSON_VALUE)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
+ .andDo(this.document.document(
+ pathParameters(parameterWithName("rolloutId").description(ApiModelPropertiesGeneric.ITEM_ID))));
+ }
+
+ @Test
+ @Description("Handles the POST request of denying a rollout. Required Permission: " + SpPermission.APPROVE_ROLLOUT)
+ public void denyRollout() throws Exception {
+ approvalStrategy.setApprovalNeeded(true);
+ final Rollout rollout = createRolloutEntity();
+ mockMvc.perform(post(MgmtRestConstants.ROLLOUT_V1_REQUEST_MAPPING + "/{rolloutId}/deny", rollout.getId())
+ .accept(MediaTypes.HAL_JSON_VALUE)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
+ .andDo(this.document.document(
+ pathParameters(parameterWithName("rolloutId").description(ApiModelPropertiesGeneric.ITEM_ID))));
+ }
+
@Test
@Description("Handles the GET request of retrieving the deploy groups of a rollout. Required Permission: "
+ SpPermission.READ_ROLLOUT)
diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TenantResourceDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TenantResourceDocumentationTest.java
index e635e51ce..d0f019b21 100644
--- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TenantResourceDocumentationTest.java
+++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TenantResourceDocumentationTest.java
@@ -80,6 +80,8 @@ public class TenantResourceDocumentationTest extends AbstractApiRestDocumentatio
"if the anonymous download mode is enabled.");
CONFIG_ITEM_DESCRIPTIONS.put(TenantConfigurationKey.REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED,
"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.");
}
@Autowired
diff --git a/hawkbit-runtime/hawkbit-update-server/src/main/resources/application.properties b/hawkbit-runtime/hawkbit-update-server/src/main/resources/application.properties
index 3c1e67c0e..3c2a50d32 100644
--- a/hawkbit-runtime/hawkbit-update-server/src/main/resources/application.properties
+++ b/hawkbit-runtime/hawkbit-update-server/src/main/resources/application.properties
@@ -42,6 +42,7 @@ spring.http.multipart.max-request-size=-1
# UI help links
hawkbit.server.ui.links.documentation.root=https://www.eclipse.org/hawkbit/documentation/overview/introduction.html
hawkbit.server.ui.links.documentation.security=https://www.eclipse.org/hawkbit/documentation/security/security.html
+hawkbit.server.ui.links.documentation.rollout=https://www.eclipse.org/hawkbit/documentation/architecture/rollout-management.html
hawkbit.server.ui.links.documentation.deployment-view=https://www.eclipse.org/hawkbit/documentation/interfaces/management-ui.html
hawkbit.server.ui.links.documentation.distribution-view=https://www.eclipse.org/hawkbit/documentation/interfaces/management-ui.html
diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java
index a2b9470c4..621610dd7 100644
--- a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java
+++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java
@@ -145,6 +145,11 @@ public final class SpPermission {
*/
public static final String HANDLE_ROLLOUT = "HANDLE_ROLLOUT";
+ /**
+ * Permission to approve or deny a rollout prior to starting.
+ */
+ public static final String APPROVE_ROLLOUT = "APPROVE_ROLLOUT";
+
private SpPermission() {
// Constants only
}
@@ -389,6 +394,14 @@ public final class SpPermission {
public static final String HAS_AUTH_ROLLOUT_MANAGEMENT_HANDLE = HAS_AUTH_PREFIX + HANDLE_ROLLOUT
+ HAS_AUTH_SUFFIX + HAS_AUTH_OR + IS_SYSTEM_CODE;
+ /**
+ * Spring security eval hasAuthority expression to check if spring
+ * context contains {@link SpPermission#APPROVE_ROLLOUT} or
+ * {@link #IS_SYSTEM_CODE}.
+ */
+ public static final String HAS_AUTH_ROLLOUT_MANAGEMENT_APPROVE = HAS_AUTH_PREFIX + APPROVE_ROLLOUT
+ + HAS_AUTH_SUFFIX + HAS_AUTH_OR + IS_SYSTEM_CODE;
+
/**
* Spring security eval hasAuthority expression to check if spring
* context contains {@link SpPermission#UPDATE_ROLLOUT} or
diff --git a/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/im/authentication/PermissionTest.java b/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/im/authentication/PermissionTest.java
index c1659e10f..38a273df1 100644
--- a/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/im/authentication/PermissionTest.java
+++ b/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/im/authentication/PermissionTest.java
@@ -31,7 +31,7 @@ public final class PermissionTest {
@Test
@Description("Verify the get permission function")
public void testGetPermissions() {
- final int allPermission = 17;
+ final int allPermission = 18;
final Collection allAuthorities = SpPermission.getAllAuthorities();
final List allAuthoritiesList = PermissionUtils.createAllAuthorityList();
assertThat(allAuthorities).hasSize(allPermission);
diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/SpPermissionChecker.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/SpPermissionChecker.java
index 8b81906e4..5b7f34aef 100644
--- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/SpPermissionChecker.java
+++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/SpPermissionChecker.java
@@ -151,4 +151,12 @@ public class SpPermissionChecker implements Serializable {
public boolean hasRolloutTargetsReadPermission() {
return hasTargetReadPermission() && permissionService.hasPermission(SpPermission.READ_ROLLOUT);
}
+
+ /**
+ *
+ * @return true if rollout can be approved by the user.
+ */
+ public boolean hasRolloutApprovalPermission() {
+ return hasRolloutReadPermission() && permissionService.hasPermission(SpPermission.APPROVE_ROLLOUT);
+ }
}
diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/UiProperties.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/UiProperties.java
index 97e19dc4f..e9efd08b9 100644
--- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/UiProperties.java
+++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/UiProperties.java
@@ -137,6 +137,11 @@ public class UiProperties implements Serializable {
*/
private String security = "";
+ /**
+ * Link to rollout related documentation.
+ */
+ private String rollout = "";
+
/**
* Link to target filter view.
*/
@@ -167,6 +172,10 @@ public class UiProperties implements Serializable {
return security;
}
+ public String getRollout() {
+ return rollout;
+ }
+
public String getSystemConfigurationView() {
return systemConfigurationView;
}
@@ -203,6 +212,10 @@ public class UiProperties implements Serializable {
this.security = security;
}
+ public void setRollout(final String rollout) {
+ this.rollout = rollout;
+ }
+
public void setSystemConfigurationView(final String systemConfigurationView) {
this.systemConfigurationView = systemConfigurationView;
}
diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/RolloutView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/RolloutView.java
index 9b9b4e57a..51b740bee 100644
--- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/RolloutView.java
+++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/RolloutView.java
@@ -19,6 +19,7 @@ import org.eclipse.hawkbit.repository.RolloutGroupManagement;
import org.eclipse.hawkbit.repository.RolloutManagement;
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
import org.eclipse.hawkbit.repository.TargetManagement;
+import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.ui.AbstractHawkbitUI;
import org.eclipse.hawkbit.ui.SpPermissionChecker;
@@ -69,16 +70,17 @@ public class RolloutView extends VerticalLayout implements View {
@Autowired
RolloutView(final SpPermissionChecker permissionChecker, final RolloutUIState rolloutUIState,
- final UIEventBus eventBus, final RolloutManagement rolloutManagement,
- final RolloutGroupManagement rolloutGroupManagement, final TargetManagement targetManagement,
- final UINotification uiNotification, final UiProperties uiProperties, final EntityFactory entityFactory,
- final VaadinMessageSource i18n, final TargetFilterQueryManagement targetFilterQueryManagement,
- final QuotaManagement quotaManagement) {
+ final UIEventBus eventBus, final RolloutManagement rolloutManagement,
+ final RolloutGroupManagement rolloutGroupManagement, final TargetManagement targetManagement,
+ final UINotification uiNotification, final UiProperties uiProperties, final EntityFactory entityFactory,
+ final VaadinMessageSource i18n, final TargetFilterQueryManagement targetFilterQueryManagement,
+ final QuotaManagement quotaManagement,
+ final TenantConfigurationManagement tenantConfigManagement) {
this.permChecker = permissionChecker;
this.rolloutManagement = rolloutManagement;
this.rolloutListView = new RolloutListView(permissionChecker, rolloutUIState, eventBus, rolloutManagement,
targetManagement, uiNotification, uiProperties, entityFactory, i18n, targetFilterQueryManagement,
- rolloutGroupManagement, quotaManagement);
+ rolloutGroupManagement, quotaManagement, tenantConfigManagement);
this.rolloutGroupsListView = new RolloutGroupsListView(i18n, eventBus, rolloutGroupManagement, rolloutUIState,
permissionChecker);
this.rolloutGroupTargetsListView = new RolloutGroupTargetsListView(eventBus, i18n, rolloutUIState);
diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AddUpdateRolloutWindowLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AddUpdateRolloutWindowLayout.java
index 556b21c95..424400213 100644
--- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AddUpdateRolloutWindowLayout.java
+++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AddUpdateRolloutWindowLayout.java
@@ -78,9 +78,11 @@ import com.vaadin.data.util.converter.StringToIntegerConverter;
import com.vaadin.data.validator.IntegerRangeValidator;
import com.vaadin.data.validator.LongRangeValidator;
import com.vaadin.data.validator.NullValidator;
+import com.vaadin.server.FontAwesome;
import com.vaadin.shared.ui.label.ContentMode;
import com.vaadin.ui.ComboBox;
import com.vaadin.ui.GridLayout;
+import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Label;
import com.vaadin.ui.OptionGroup;
import com.vaadin.ui.TabSheet;
@@ -106,6 +108,10 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
private static final String MESSAGE_ENTER_NUMBER = "message.enter.number";
+ private static final String APPROVAL_BUTTON_LABEL = "button.approve";
+
+ private static final String DENY_BUTTON_LABEL = "button.deny";
+
private final ActionTypeOptionGroupLayout actionTypeOptionGroupLayout;
private final AutoStartOptionGroupLayout autoStartOptionGroupLayout;
@@ -150,6 +156,10 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
private OptionGroup errorThresholdOptionGroup;
+ private Label approvalLabel;
+
+ private HorizontalLayout approvalButtonsLayout;
+
private CommonDialogWindow window;
private boolean editRolloutEnabled;
@@ -166,6 +176,10 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
private GroupsLegendLayout groupsLegendLayout;
+ private OptionGroup approveButtonsGroup;
+
+ private TextField approvalRemarkField;
+
private final transient RolloutGroupConditions defaultRolloutGroupConditions;
private final NullValidator nullValidator = new NullValidator(null, false);
@@ -213,6 +227,11 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
if (editRolloutEnabled) {
editRollout();
+ if (rollout.getStatus().equals(Rollout.RolloutStatus.WAITING_FOR_APPROVAL)) {
+ rolloutManagement.approveOrDeny(rollout.getId(),
+ (Rollout.ApprovalDecision) approveButtonsGroup.getValue(), approvalRemarkField.getValue());
+ eventBus.publish(this, RolloutEvent.UPDATE_ROLLOUT);
+ }
return;
}
createRollout();
@@ -413,6 +432,12 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
rollout = null;
groupsDefinitionTabs.setVisible(true);
groupsDefinitionTabs.setSelectedTab(0);
+
+ approvalLabel.setVisible(false);
+ approvalButtonsLayout.setVisible(false);
+ approveButtonsGroup.clear();
+ approvalRemarkField.clear();
+ approveButtonsGroup.removeAllValidators();
}
private void addGroupsDefinitionTabs() {
@@ -443,7 +468,7 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
setSpacing(true);
setSizeUndefined();
- setRows(7);
+ setRows(8);
setColumns(4);
setStyleName("marginTop");
setColumnExpandRatio(3, 1);
@@ -477,6 +502,9 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
addComponent(groupsDefinitionTabs, 0, 6, 3, 6);
+ addComponent(approvalLabel, 0, 7);
+ addComponent(approvalButtonsLayout, 1, 7, 3, 7);
+
rolloutName.focus();
}
@@ -541,6 +569,8 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
groupsLegendLayout = new GroupsLegendLayout(i18n);
+ approvalLabel = getLabel("label.approval.decision");
+ approvalButtonsLayout = createApprovalLayout();
}
private void displayValidationStatus(final DefineGroupsLayout.ValidationStatus status) {
@@ -610,6 +640,29 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
return layout;
}
+ private HorizontalLayout createApprovalLayout() {
+ approveButtonsGroup = new OptionGroup();
+ approveButtonsGroup.setId(UIComponentIdProvider.ROLLOUT_APPROVAL_OPTIONGROUP_ID);
+ approveButtonsGroup.addStyleName(ValoTheme.OPTIONGROUP_SMALL);
+ approveButtonsGroup.addStyleName(ValoTheme.OPTIONGROUP_HORIZONTAL);
+ approveButtonsGroup.addStyleName("custom-option-group");
+ approveButtonsGroup.addItems(Rollout.ApprovalDecision.APPROVED, Rollout.ApprovalDecision.DENIED);
+
+ approveButtonsGroup.setItemCaption(Rollout.ApprovalDecision.APPROVED, i18n.getMessage(APPROVAL_BUTTON_LABEL));
+ approveButtonsGroup.setItemIcon(Rollout.ApprovalDecision.APPROVED, FontAwesome.CHECK);
+ approveButtonsGroup.setItemCaption(Rollout.ApprovalDecision.DENIED, i18n.getMessage(DENY_BUTTON_LABEL));
+ approveButtonsGroup.setItemIcon(Rollout.ApprovalDecision.DENIED, FontAwesome.TIMES);
+
+ approvalRemarkField = createTextField("label.approval.remark",
+ UIComponentIdProvider.ROLLOUT_APPROVAL_REMARK_FIELD_ID, Rollout.APPROVAL_REMARK_MAX_SIZE);
+ approvalRemarkField.setWidth(100.0F, Unit.PERCENTAGE);
+
+ HorizontalLayout layout = new HorizontalLayout(approveButtonsGroup, approvalRemarkField);
+ layout.setWidth(100.0F, Unit.PERCENTAGE);
+ layout.setExpandRatio(approvalRemarkField, 1.0F);
+ return layout;
+ }
+
private static Label createCountLabel() {
final Label groupSize = new LabelBuilder().visible(false).name("").buildLabel();
groupSize.addStyleName(ValoTheme.LABEL_TINY + " " + "rollout-target-count-message");
@@ -966,6 +1019,13 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
if (rollout.getStatus() != Rollout.RolloutStatus.READY) {
disableRequiredFieldsOnEdit();
}
+
+ if (rollout.getStatus() == Rollout.RolloutStatus.WAITING_FOR_APPROVAL) {
+ approvalButtonsLayout.setVisible(true);
+ approveButtonsGroup.addValidator(nullValidator);
+ approvalLabel.setVisible(true);
+ }
+
rolloutName.setValue(rollout.getName());
groupsDefinitionTabs.setVisible(false);
diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/ProxyRollout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/ProxyRollout.java
index 4124d90e9..e1a6a3b01 100644
--- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/ProxyRollout.java
+++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/ProxyRollout.java
@@ -43,6 +43,8 @@ public class ProxyRollout {
private long forcedTime;
private RolloutStatus status;
private TotalTargetCountStatus totalTargetCountStatus;
+ private String approvalDecidedBy;
+ private String approvalRemark;
public RolloutRendererData getRolloutRendererData() {
return rolloutRendererData;
@@ -217,4 +219,20 @@ public class ProxyRollout {
public void setTotalTargetCountStatus(final TotalTargetCountStatus totalTargetCountStatus) {
this.totalTargetCountStatus = totalTargetCountStatus;
}
+
+ public String getApprovalDecidedBy() {
+ return approvalDecidedBy;
+ }
+
+ public void setApprovalDecidedBy(final String approvalDecidedBy) {
+ this.approvalDecidedBy = approvalDecidedBy;
+ }
+
+ public String getApprovalRemark() {
+ return approvalRemark;
+ }
+
+ public void setApprovalRemark(final String approvalRemark) {
+ this.approvalRemark = approvalRemark;
+ }
}
diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutBeanQuery.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutBeanQuery.java
index 647d168f2..d34714991 100644
--- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutBeanQuery.java
+++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutBeanQuery.java
@@ -124,6 +124,8 @@ public class RolloutBeanQuery extends AbstractBeanQuery {
final TotalTargetCountStatus totalTargetCountActionStatus = rollout.getTotalTargetCountStatus();
proxyRollout.setTotalTargetCountStatus(totalTargetCountActionStatus);
proxyRollout.setTotalTargetsCount(String.valueOf(rollout.getTotalTargets()));
+ proxyRollout.setApprovalDecidedBy(rollout.getApprovalDecidedBy());
+ proxyRollout.setApprovalRemark(rollout.getApprovalRemark());
return proxyRollout;
}
diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListGrid.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListGrid.java
index d522ad87b..46e4b59aa 100644
--- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListGrid.java
+++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListGrid.java
@@ -8,9 +8,11 @@
*/
package org.eclipse.hawkbit.ui.rollout.rollout;
+import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.*;
import static org.eclipse.hawkbit.ui.rollout.DistributionBarHelper.getTooltip;
import java.util.Arrays;
+import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Locale;
@@ -24,6 +26,7 @@ import org.eclipse.hawkbit.repository.RolloutGroupManagement;
import org.eclipse.hawkbit.repository.RolloutManagement;
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
import org.eclipse.hawkbit.repository.TargetManagement;
+import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus;
import org.eclipse.hawkbit.repository.model.TotalTargetCountStatus;
@@ -77,6 +80,7 @@ public class RolloutListGrid extends AbstractGrid {
private static final String ROLLOUT_RENDERER_DATA = "rolloutRendererData";
+ private static final String VIRT_PROP_APPROVE = "approve";
private static final String VIRT_PROP_RUN = "run";
private static final String VIRT_PROP_PAUSE = "pause";
private static final String VIRT_PROP_UPDATE = "update";
@@ -87,6 +91,8 @@ public class RolloutListGrid extends AbstractGrid {
private final transient RolloutGroupManagement rolloutGroupManagement;
+ private final transient TenantConfigurationManagement tenantConfigManagement;
+
private final AddUpdateRolloutWindowLayout addUpdateRolloutWindow;
private final UINotification uiNotification;
@@ -95,7 +101,8 @@ public class RolloutListGrid extends AbstractGrid {
private static final List DELETE_COPY_BUTTON_ENABLED = Arrays.asList(RolloutStatus.CREATING,
RolloutStatus.ERROR_CREATING, RolloutStatus.ERROR_STARTING, RolloutStatus.PAUSED, RolloutStatus.READY,
- RolloutStatus.RUNNING, RolloutStatus.STARTING, RolloutStatus.STOPPED, RolloutStatus.FINISHED);
+ RolloutStatus.RUNNING, RolloutStatus.STARTING, RolloutStatus.STOPPED, RolloutStatus.FINISHED,
+ RolloutStatus.WAITING_FOR_APPROVAL, RolloutStatus.APPROVAL_DENIED);
private static final List UPDATE_BUTTON_ENABLED = Arrays.asList(RolloutStatus.CREATING,
RolloutStatus.ERROR_CREATING, RolloutStatus.ERROR_STARTING, RolloutStatus.PAUSED, RolloutStatus.READY,
@@ -106,11 +113,14 @@ public class RolloutListGrid extends AbstractGrid {
private static final List RUN_BUTTON_ENABLED = Arrays.asList(RolloutStatus.READY,
RolloutStatus.PAUSED);
+ private static final List APPROVE_BUTTON_ENABLED = Collections.singletonList(RolloutStatus.WAITING_FOR_APPROVAL);
+
private static final Map statusIconMap = new EnumMap<>(RolloutStatus.class);
private static final List