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 HIDDEN_COLUMNS = Arrays.asList(SPUILabelDefinitions.VAR_CREATED_DATE, SPUILabelDefinitions.VAR_CREATED_USER, SPUILabelDefinitions.VAR_MODIFIED_DATE, - SPUILabelDefinitions.VAR_MODIFIED_BY, SPUILabelDefinitions.VAR_DESC); + SPUILabelDefinitions.VAR_MODIFIED_BY, SPUILabelDefinitions.VAR_APPROVAL_DECIDED_BY, + SPUILabelDefinitions.VAR_APPROVAL_REMARK, SPUILabelDefinitions.VAR_DESC); static { statusIconMap.put(RolloutStatus.FINISHED, @@ -118,6 +128,10 @@ public class RolloutListGrid extends AbstractGrid { statusIconMap.put(RolloutStatus.PAUSED, new StatusFontIcon(FontAwesome.PAUSE, SPUIStyleDefinitions.STATUS_ICON_BLUE)); statusIconMap.put(RolloutStatus.RUNNING, new StatusFontIcon(null, SPUIStyleDefinitions.STATUS_SPINNER_YELLOW)); + statusIconMap.put(RolloutStatus.WAITING_FOR_APPROVAL, + new StatusFontIcon(FontAwesome.HOURGLASS_HALF, SPUIStyleDefinitions.STATUS_ICON_ORANGE)); + statusIconMap.put(RolloutStatus.APPROVAL_DENIED, + new StatusFontIcon(FontAwesome.TIMES_CIRCLE, SPUIStyleDefinitions.STATUS_ICON_RED)); statusIconMap.put(RolloutStatus.READY, new StatusFontIcon(FontAwesome.DOT_CIRCLE_O, SPUIStyleDefinitions.STATUS_ICON_LIGHT_BLUE)); statusIconMap.put(RolloutStatus.STOPPED, @@ -132,14 +146,17 @@ public class RolloutListGrid extends AbstractGrid { } RolloutListGrid(final VaadinMessageSource i18n, final UIEventBus eventBus, - final RolloutManagement rolloutManagement, final UINotification uiNotification, - final RolloutUIState rolloutUIState, final SpPermissionChecker permissionChecker, - final TargetManagement targetManagement, final EntityFactory entityFactory, final UiProperties uiProperties, - final TargetFilterQueryManagement targetFilterQueryManagement, - final RolloutGroupManagement rolloutGroupManagement, final QuotaManagement quotaManagement) { + final RolloutManagement rolloutManagement, final UINotification uiNotification, + final RolloutUIState rolloutUIState, final SpPermissionChecker permissionChecker, + final TargetManagement targetManagement, final EntityFactory entityFactory, + final UiProperties uiProperties, + final TargetFilterQueryManagement targetFilterQueryManagement, + final RolloutGroupManagement rolloutGroupManagement, final QuotaManagement quotaManagement, + final TenantConfigurationManagement tenantConfigManagement) { super(i18n, eventBus, permissionChecker); this.rolloutManagement = rolloutManagement; this.rolloutGroupManagement = rolloutGroupManagement; + this.tenantConfigManagement = tenantConfigManagement; this.addUpdateRolloutWindow = new AddUpdateRolloutWindowLayout(rolloutManagement, targetManagement, uiNotification, uiProperties, entityFactory, i18n, eventBus, targetFilterQueryManagement, rolloutGroupManagement, quotaManagement); @@ -250,7 +267,10 @@ public class RolloutListGrid extends AbstractGrid { false); rolloutGridContainer.addContainerProperty(SPUILabelDefinitions.VAR_CREATED_DATE, String.class, null, false, false); - + rolloutGridContainer.addContainerProperty(SPUILabelDefinitions.VAR_APPROVAL_DECIDED_BY, String.class, null, false, + false); + rolloutGridContainer.addContainerProperty(SPUILabelDefinitions.VAR_APPROVAL_REMARK, String.class, null, false, + false); rolloutGridContainer.addContainerProperty(SPUILabelDefinitions.VAR_MODIFIED_DATE, String.class, null, false, false); rolloutGridContainer.addContainerProperty(SPUILabelDefinitions.VAR_CREATED_USER, String.class, null, false, @@ -289,6 +309,9 @@ public class RolloutListGrid extends AbstractGrid { getColumn(VIRT_PROP_RUN).setMinimumWidth(25); getColumn(VIRT_PROP_RUN).setMaximumWidth(25); + getColumn(VIRT_PROP_APPROVE).setMinimumWidth(25); + getColumn(VIRT_PROP_APPROVE).setMaximumWidth(25); + getColumn(VIRT_PROP_PAUSE).setMinimumWidth(25); getColumn(VIRT_PROP_PAUSE).setMaximumWidth(25); @@ -315,12 +338,15 @@ public class RolloutListGrid extends AbstractGrid { getColumn(SPUILabelDefinitions.VAR_CREATED_USER).setHeaderCaption(i18n.getMessage("header.createdBy")); getColumn(SPUILabelDefinitions.VAR_MODIFIED_DATE).setHeaderCaption(i18n.getMessage("header.modifiedDate")); getColumn(SPUILabelDefinitions.VAR_MODIFIED_BY).setHeaderCaption(i18n.getMessage("header.modifiedBy")); + getColumn(SPUILabelDefinitions.VAR_APPROVAL_REMARK).setHeaderCaption(i18n.getMessage("header.approvalRemark")); + getColumn(SPUILabelDefinitions.VAR_APPROVAL_DECIDED_BY).setHeaderCaption(i18n.getMessage("header.approvalDecidedBy")); getColumn(SPUILabelDefinitions.VAR_DESC).setHeaderCaption(i18n.getMessage("header.description")); getColumn(SPUILabelDefinitions.VAR_TOTAL_TARGETS_COUNT_STATUS) .setHeaderCaption(i18n.getMessage("header.detail.status")); getColumn(SPUILabelDefinitions.VAR_STATUS).setHeaderCaption(i18n.getMessage("header.status")); getColumn(VIRT_PROP_RUN).setHeaderCaption(i18n.getMessage("header.action.run")); + getColumn(VIRT_PROP_APPROVE).setHeaderCaption(i18n.getMessage("header.action.approve")); getColumn(VIRT_PROP_PAUSE).setHeaderCaption(i18n.getMessage("header.action.pause")); getColumn(VIRT_PROP_UPDATE).setHeaderCaption(i18n.getMessage("header.action.update")); getColumn(VIRT_PROP_COPY).setHeaderCaption(i18n.getMessage("header.action.copy")); @@ -331,7 +357,7 @@ public class RolloutListGrid extends AbstractGrid { private HeaderCell joinColumns() { - return getDefaultHeaderRow().join(VIRT_PROP_RUN, VIRT_PROP_PAUSE, VIRT_PROP_UPDATE, VIRT_PROP_COPY, + return getDefaultHeaderRow().join(VIRT_PROP_RUN, VIRT_PROP_APPROVE, VIRT_PROP_PAUSE, VIRT_PROP_UPDATE, VIRT_PROP_COPY, VIRT_PROP_DELETE); } @@ -346,10 +372,11 @@ public class RolloutListGrid extends AbstractGrid { final List columnsToShowInOrder = Arrays.asList(ROLLOUT_RENDERER_DATA, SPUILabelDefinitions.VAR_DIST_NAME_VERSION, SPUILabelDefinitions.VAR_STATUS, SPUILabelDefinitions.VAR_TOTAL_TARGETS_COUNT_STATUS, SPUILabelDefinitions.VAR_NUMBER_OF_GROUPS, - SPUILabelDefinitions.VAR_TOTAL_TARGETS, VIRT_PROP_RUN, VIRT_PROP_PAUSE, VIRT_PROP_UPDATE, + SPUILabelDefinitions.VAR_TOTAL_TARGETS, VIRT_PROP_APPROVE, VIRT_PROP_RUN, VIRT_PROP_PAUSE, VIRT_PROP_UPDATE, VIRT_PROP_COPY, VIRT_PROP_DELETE, SPUILabelDefinitions.VAR_CREATED_DATE, SPUILabelDefinitions.VAR_CREATED_USER, SPUILabelDefinitions.VAR_MODIFIED_DATE, - SPUILabelDefinitions.VAR_MODIFIED_BY, SPUILabelDefinitions.VAR_DESC); + SPUILabelDefinitions.VAR_MODIFIED_BY, SPUILabelDefinitions.VAR_APPROVAL_DECIDED_BY, + SPUILabelDefinitions.VAR_APPROVAL_REMARK, SPUILabelDefinitions.VAR_DESC); setColumns(columnsToShowInOrder.toArray()); } @@ -361,6 +388,7 @@ public class RolloutListGrid extends AbstractGrid { } getColumn(VIRT_PROP_RUN).setHidable(false); + getColumn(VIRT_PROP_APPROVE).setHidable(false); getColumn(VIRT_PROP_PAUSE).setHidable(false); getColumn(VIRT_PROP_DELETE).setHidable(false); getColumn(VIRT_PROP_UPDATE).setHidable(false); @@ -388,6 +416,9 @@ public class RolloutListGrid extends AbstractGrid { getColumn(VIRT_PROP_RUN).setRenderer( new GridButtonRenderer(clickEvent -> startOrResumeRollout((Long) clickEvent.getItemId())), new RolloutGridButtonConverter(this::createRunButtonMetadata)); + getColumn(VIRT_PROP_APPROVE).setRenderer( + new GridButtonRenderer(clickEvent -> approveRollout((Long) clickEvent.getItemId())), + new RolloutGridButtonConverter(this::createApprovalButtonMetadata)); getColumn(VIRT_PROP_PAUSE).setRenderer( new GridButtonRenderer(clickEvent -> pauseRollout((Long) clickEvent.getItemId())), new RolloutGridButtonConverter(this::createPauseButtonMetadata)); @@ -440,6 +471,7 @@ public class RolloutListGrid extends AbstractGrid { final GeneratedPropertyContainer decoratedContainer = getDecoratedContainer(); decoratedContainer.addGeneratedProperty(VIRT_PROP_RUN, new GenericPropertyValueGenerator()); + decoratedContainer.addGeneratedProperty(VIRT_PROP_APPROVE, new GenericPropertyValueGenerator()); decoratedContainer.addGeneratedProperty(VIRT_PROP_PAUSE, new GenericPropertyValueGenerator()); decoratedContainer.addGeneratedProperty(VIRT_PROP_UPDATE, new GenericPropertyValueGenerator()); decoratedContainer.addGeneratedProperty(VIRT_PROP_COPY, new GenericPropertyValueGenerator()); @@ -520,6 +552,13 @@ public class RolloutListGrid extends AbstractGrid { } } + private void approveRollout(final Long rolloutId) { + final CommonDialogWindow addTargetWindow = addUpdateRolloutWindow.getWindow(rolloutId, false); + addTargetWindow.setCaption(i18n.getMessage("caption.approve.rollout")); + UI.getCurrent().addWindow(addTargetWindow); + addTargetWindow.setVisible(Boolean.TRUE); + } + private void updateRollout(final Long rolloutId) { final CommonDialogWindow addTargetWindow = addUpdateRolloutWindow.getWindow(rolloutId, false); addTargetWindow.setCaption(i18n.getMessage("caption.update.rollout")); @@ -578,7 +617,7 @@ public class RolloutListGrid extends AbstractGrid { String description = null; if (SPUILabelDefinitions.VAR_STATUS.equals(cell.getPropertyId())) { - description = cell.getProperty().getValue().toString().toLowerCase(); + description = cell.getProperty().getValue().toString().toLowerCase().replace("_", " "); } else if (SPUILabelDefinitions.ACTION.equals(cell.getPropertyId())) { description = SPUILabelDefinitions.ACTION.toLowerCase(); } else if (ROLLOUT_RENDERER_DATA.equals(cell.getPropertyId())) { @@ -595,6 +634,12 @@ public class RolloutListGrid extends AbstractGrid { return !expectedRolloutStatus.contains(currentRolloutStatus); } + private StatusFontIcon createApprovalButtonMetadata(final RolloutStatus rolloutStatus) { + final boolean isDisabled = hasToBeDisabled(rolloutStatus, APPROVE_BUTTON_ENABLED); + return new StatusFontIcon(FontAwesome.GAVEL, null, i18n.getMessage("tooltip.rollout.approve"), + UIComponentIdProvider.ROLLOUT_APPROVAL_BUTTON_ID, isDisabled); + } + private StatusFontIcon createRunButtonMetadata(final RolloutStatus rolloutStatus) { final boolean isDisabled = hasToBeDisabled(rolloutStatus, RUN_BUTTON_ENABLED); return new StatusFontIcon(FontAwesome.PLAY, null, i18n.getMessage("tooltip.rollout.run"), @@ -744,6 +789,11 @@ public class RolloutListGrid extends AbstractGrid { if (!permissionChecker.hasRolloutCreatePermission()) { modifiableColumnsList.remove(VIRT_PROP_COPY); } + if (!permissionChecker.hasRolloutApprovalPermission() || + !tenantConfigManagement.getConfigurationValue( + TenantConfigurationKey.ROLLOUT_APPROVAL_ENABLED, Boolean.class).getValue()) { + modifiableColumnsList.remove(VIRT_PROP_APPROVE); + } if (!permissionChecker.hasRolloutDeletePermission()) { modifiableColumnsList.remove(VIRT_PROP_DELETE); } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListView.java index 85327b1eb..914486022 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListView.java @@ -14,6 +14,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.ui.SpPermissionChecker; import org.eclipse.hawkbit.ui.UiProperties; import org.eclipse.hawkbit.ui.common.grid.AbstractGrid; @@ -38,6 +39,7 @@ public class RolloutListView extends AbstractGridComponentLayout { private final transient EntityFactory entityFactory; private final transient TargetFilterQueryManagement targetFilterQueryManagement; private final transient QuotaManagement quotaManagement; + private final transient TenantConfigurationManagement tenantConfigManagement; private final SpPermissionChecker permissionChecker; private final RolloutUIState rolloutUIState; @@ -45,11 +47,13 @@ public class RolloutListView extends AbstractGridComponentLayout { private final UiProperties uiProperties; public RolloutListView(final SpPermissionChecker permissionChecker, final RolloutUIState rolloutUIState, - final UIEventBus eventBus, final RolloutManagement rolloutManagement, - final TargetManagement targetManagement, final UINotification uiNotification, - final UiProperties uiProperties, final EntityFactory entityFactory, final VaadinMessageSource i18n, - final TargetFilterQueryManagement targetFilterQueryManagement, - final RolloutGroupManagement rolloutGroupManagement, final QuotaManagement quotaManagement) { + final UIEventBus eventBus, final RolloutManagement rolloutManagement, + final TargetManagement targetManagement, final UINotification uiNotification, + final UiProperties uiProperties, final EntityFactory entityFactory, + final VaadinMessageSource i18n, + final TargetFilterQueryManagement targetFilterQueryManagement, + final RolloutGroupManagement rolloutGroupManagement, final QuotaManagement quotaManagement, + final TenantConfigurationManagement tenantConfigManagement) { super(i18n, eventBus); this.permissionChecker = permissionChecker; this.rolloutUIState = rolloutUIState; @@ -61,6 +65,7 @@ public class RolloutListView extends AbstractGridComponentLayout { this.uiProperties = uiProperties; this.entityFactory = entityFactory; this.targetFilterQueryManagement = targetFilterQueryManagement; + this.tenantConfigManagement = tenantConfigManagement; init(); } @@ -76,7 +81,7 @@ public class RolloutListView extends AbstractGridComponentLayout { public AbstractGrid createGrid() { return new RolloutListGrid(i18n, eventBus, rolloutManagement, uiNotification, rolloutUIState, permissionChecker, targetManagement, entityFactory, uiProperties, targetFilterQueryManagement, rolloutGroupManagement, - quotaManagement); + quotaManagement, tenantConfigManagement); } } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/RolloutConfigurationView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/RolloutConfigurationView.java new file mode 100644 index 000000000..987ec08b5 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/RolloutConfigurationView.java @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2018 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.tenantconfiguration; + +import com.vaadin.data.Property; +import com.vaadin.ui.CheckBox; +import com.vaadin.ui.GridLayout; +import com.vaadin.ui.Label; +import com.vaadin.ui.Link; +import com.vaadin.ui.Panel; +import com.vaadin.ui.VerticalLayout; +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; +import org.eclipse.hawkbit.ui.UiProperties; +import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; +import org.eclipse.hawkbit.ui.tenantconfiguration.rollout.ApprovalConfigurationItem; +import org.eclipse.hawkbit.ui.utils.UIComponentIdProvider; +import org.eclipse.hawkbit.ui.utils.VaadinMessageSource; + +/** + * Provides configuration of the RolloutManagement including enabling/disabling + * of the approval workflow. + */ +public class RolloutConfigurationView extends BaseConfigurationView + implements Property.ValueChangeListener, ConfigurationItem.ConfigurationItemChangeListener { + + private static final long serialVersionUID = 1L; + + private final ApprovalConfigurationItem approvalConfigurationItem; + private final VaadinMessageSource i18n; + private final UiProperties uiProperties; + private CheckBox approvalCheckbox; + + RolloutConfigurationView(final VaadinMessageSource i18n, + final TenantConfigurationManagement tenantConfigurationManagement, final UiProperties uiProperties) { + this.i18n = i18n; + this.uiProperties = uiProperties; + this.approvalConfigurationItem = new ApprovalConfigurationItem(tenantConfigurationManagement, i18n); + this.init(); + } + + private void init() { + + final Panel rootPanel = new Panel(); + rootPanel.setSizeFull(); + + rootPanel.addStyleName("config-panel"); + + final VerticalLayout vLayout = new VerticalLayout(); + vLayout.setMargin(true); + vLayout.setSizeFull(); + + final Label headerDisSetType = new Label(i18n.getMessage("configuration.rollout.title")); + headerDisSetType.addStyleName("config-panel-header"); + vLayout.addComponent(headerDisSetType); + + final Link linkToApprovalHelp = SPUIComponentProvider + .getHelpLink(uiProperties.getLinks().getDocumentation().getRollout()); + vLayout.addComponent(linkToApprovalHelp); + + final GridLayout gridLayout = new GridLayout(2, 1); + gridLayout.setSpacing(true); + gridLayout.setImmediate(true); + gridLayout.setColumnExpandRatio(1, 1.0F); + + approvalCheckbox = SPUIComponentProvider.getCheckBox("", "", null, false, ""); + approvalCheckbox.setId(UIComponentIdProvider.ROLLOUT_APPROVAL_ENABLED_CHECKBOX); + approvalCheckbox.setValue(approvalConfigurationItem.isConfigEnabled()); + approvalCheckbox.addValueChangeListener(this); + approvalConfigurationItem.addChangeListener(this); + gridLayout.addComponent(approvalCheckbox, 0, 0); + gridLayout.addComponent(approvalConfigurationItem, 1, 0); + + vLayout.addComponent(gridLayout); + rootPanel.setContent(vLayout); + setCompositionRoot(rootPanel); + } + + @Override + public void save() { + this.approvalConfigurationItem.save(); + } + + @Override + public void undo() { + this.approvalConfigurationItem.undo(); + } + + @Override + public void valueChange(Property.ValueChangeEvent event) { + if (approvalCheckbox.equals(event.getProperty())) { + if (approvalCheckbox.getValue()) { + approvalConfigurationItem.configEnable(); + } else { + approvalConfigurationItem.configDisable(); + } + notifyConfigurationChanged(); + } + } + + @Override + public void configurationHasChanged() { + notifyConfigurationChanged(); + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/TenantConfigurationDashboardView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/TenantConfigurationDashboardView.java index eb2750744..25fa046aa 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/TenantConfigurationDashboardView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/TenantConfigurationDashboardView.java @@ -62,6 +62,8 @@ public class TenantConfigurationDashboardView extends CustomComponent implements private final PollingConfigurationView pollingConfigurationView; + private final RolloutConfigurationView rolloutConfigurationView; + private final VaadinMessageSource i18n; private final UiProperties uiProperties; @@ -90,6 +92,7 @@ public class TenantConfigurationDashboardView extends CustomComponent implements this.pollingConfigurationView = new PollingConfigurationView(i18n, controllerPollProperties, tenantConfigurationManagement); this.repositoryConfigurationView = new RepositoryConfigurationView(i18n, tenantConfigurationManagement); + this.rolloutConfigurationView = new RolloutConfigurationView(i18n, tenantConfigurationManagement, uiProperties); this.i18n = i18n; this.uiProperties = uiProperties; @@ -105,7 +108,7 @@ public class TenantConfigurationDashboardView extends CustomComponent implements configurationViews.add(defaultDistributionSetTypeLayout); } configurationViews.add(repositoryConfigurationView); - + configurationViews.add(rolloutConfigurationView); configurationViews.add(authenticationConfigurationView); configurationViews.add(pollingConfigurationView); if (customConfigurationViews != null) { diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/rollout/ApprovalConfigurationItem.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/rollout/ApprovalConfigurationItem.java new file mode 100644 index 000000000..f9dddb021 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/rollout/ApprovalConfigurationItem.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2018 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.tenantconfiguration.rollout; + +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; +import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey; +import org.eclipse.hawkbit.ui.tenantconfiguration.generic.AbstractBooleanTenantConfigurationItem; +import org.eclipse.hawkbit.ui.utils.VaadinMessageSource; + +/** + * This class represents the UI item for the target security token section in + * the authentication configuration view. + */ +public class ApprovalConfigurationItem extends AbstractBooleanTenantConfigurationItem { + + private static final long serialVersionUID = 1L; + + private boolean configurationEnabled; + private boolean configurationEnabledChange; + + /** + * Constructor for tenant specific approval mode setting. + * + * @param tenantConfigurationManagement used to enable/disable the approval mode per tenant + * @param i18n used to translate labels + */ + public ApprovalConfigurationItem(final TenantConfigurationManagement tenantConfigurationManagement, + final VaadinMessageSource i18n) { + super(TenantConfigurationKey.ROLLOUT_APPROVAL_ENABLED, tenantConfigurationManagement, i18n); + + super.init("configuration.rollout.approval.label"); + configurationEnabled = isConfigEnabled(); + } + + @Override + public void configEnable() { + if (!configurationEnabled) { + configurationEnabledChange = true; + } + configurationEnabled = true; + } + + @Override + public void configDisable() { + if (configurationEnabled) { + configurationEnabledChange = true; + } + configurationEnabled = false; + } + + @Override + public void save() { + if (!configurationEnabledChange) { + return; + } + getTenantConfigurationManagement().addOrUpdateConfiguration(getConfigurationKey(), configurationEnabled); + } + + @Override + public void undo() { + configurationEnabledChange = false; + configurationEnabled = getTenantConfigurationManagement() + .getConfigurationValue(getConfigurationKey(), Boolean.class).getValue(); + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/SPUILabelDefinitions.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/SPUILabelDefinitions.java index 05c3a0ad8..469340f1b 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/SPUILabelDefinitions.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/SPUILabelDefinitions.java @@ -133,6 +133,16 @@ public final class SPUILabelDefinitions { * Last modified date. */ public static final String VAR_MODIFIED_DATE = "modifiedDate"; + + /** + * Approve/Deny remark. + */ + public static final String VAR_APPROVAL_REMARK = "approvalRemark"; + /** + * Approval decider. + */ + public static final String VAR_APPROVAL_DECIDED_BY = "approvalDecidedBy"; + /** * Poll Status. */ diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java index 4fdc6d795..0a57647c5 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java @@ -866,6 +866,16 @@ public final class UIComponentIdProvider { */ public static final String ROLLOUT_RUN_BUTTON_ID = ROLLOUT_ACTION_ID + ".6"; + /** + * Rollout approval button id. + */ + public static final String ROLLOUT_APPROVAL_BUTTON_ID = ROLLOUT_ACTION_ID + ".11"; + + /** + * Rollout approve/deny option button group id. + */ + public static final String ROLLOUT_APPROVAL_OPTIONGROUP_ID = ROLLOUT_ACTION_ID + ".12"; + /** * Rollout pause button id. */ @@ -1082,6 +1092,17 @@ public final class UIComponentIdProvider { */ public static final String REPOSITORY_ACTIONS_AUTOCLOSE_CHECKBOX = "repositoryactionsautoclosecheckbox"; + /** + * Configuration checkbox for + * {@link TenantConfigurationKey#ROLLOUT_APPROVAL_ENABLED} + */ + public static final String ROLLOUT_APPROVAL_ENABLED_CHECKBOX = "rollout.approve.enabled.checkbox"; + + /** + * Id of the rollout approval remark field + */ + public static final String ROLLOUT_APPROVAL_REMARK_FIELD_ID = "rollout.approve.remark"; + /** * /* Private Constructor. */ diff --git a/hawkbit-ui/src/main/resources/messages.properties b/hawkbit-ui/src/main/resources/messages.properties index 1b246a3a7..c5878338f 100644 --- a/hawkbit-ui/src/main/resources/messages.properties +++ b/hawkbit-ui/src/main/resources/messages.properties @@ -38,6 +38,9 @@ button.cancel = Cancel button.upload.file = Upload File button.no.auto.assignment = none button.auto.assignment.desc = Select auto assign distribution set +button.approve = Approve +button.deny = Deny +button.apply = Apply bulk.targets.upload = Please upload csv file. bulkupload.ds.name = DS Name button.discard=Discard @@ -72,6 +75,7 @@ header.swmodules=SwModules header.migrations.step=IsRequiredMigrationStep header.action=Actions header.action.run=Run +header.action.approve=Approve header.action.pause=Pause header.action.update=Edit header.action.copy=Copy @@ -219,6 +223,8 @@ label.unsupported.browser.ie=Sorry! current browser is not supported. Please use label.auto.assign.description=When an auto assign distribution set is selected, it will be automatically assigned to all targets that match the target filter. label.auto.assign.enable=Enable auto assignment label.scheduled=Scheduled +label.approval.decision = Approval decision +label.approval.remark = Remark (optional) # Checkbox label prefix with - checkbox checkbox.dist.migration.required = Required Migration Step : @@ -254,6 +260,7 @@ tooltip.metadata.icon = Manage Metadata tooltip.next.maintenancewindow = next on {0} #rollout action tooltip.rollout.run = Run +tooltip.rollout.approve = Approve tooltip.rollout.pause = Pause tooltip.rollout.update = Update.. tooltip.rollout.copy = Copy.. @@ -483,6 +490,8 @@ configuration.polling.title=Polling Configuration configuration.polling.time=Polling Time configuration.polling.overduetime=Polling Overdue Time configuration.polling.custom.value=use a custom value +configuration.rollout.title=Rollout Configuration +configuration.rollout.approval.label=Approve rollout before it can be started #Calendar calendar.year=year @@ -511,13 +520,15 @@ header.assigned.ds = Assigned DS header.installed.ds = Installed DS header.target.status = Status header.target.tags = Tags -header.total.targets = Targets +header.total.targets = Targets header.key = Key header.value = Value metadata.targetvisible = Visible for targets header.auto.assignment.ds = Auto assignment header.target.filter.name = Target filter name header.target.filter.query = Target filter query +header.approvalDecidedBy = Decided By +header.approvalRemark = Approval Remark distribution.details.header = Distribution set target.details.header = Target @@ -556,6 +567,7 @@ caption.configure.rollout = Configure Rollout caption.configure.rollout.groups = Configure Deployment Groups caption.update.rollout = Update Rollout caption.create.rollout = Create new Rollout +caption.approve.rollout = Approve Rollout prompt.target.filter = Custom Target Filter message.rollout.nonzero.group.number = Number of groups must be greater than zero message.rollout.max.group.number = Number of groups must not be greater than 500