Feature Approval Workflow for rollouts (#678)

* implement feature approval workflow

Signed-off-by: Ioannis Spyropoulos <ioannis.spyropoulos@bosch-si.com>
Signed-off-by: Michael Müller <Michael.Mueller17@bosch-si.com>

* add documentation for REST endpoints

Signed-off-by: Ioannis Spyropoulos <ioannis.spyropoulos@bosch-si.com>
Signed-off-by: Michael Müller <Michael.Mueller17@bosch-si.com>

* fix broken documentation test

Signed-off-by: Ioannis Spyropoulos <ioannis.spyropoulos@bosch-si.com>

* fix broken documentation test II

Signed-off-by: Ioannis Spyropoulos <ioannis.spyropoulos@bosch-si.com>
This commit is contained in:
Michael Müller
2018-06-04 16:36:56 +02:00
committed by Dominic Schabel
parent 8a14c931c8
commit cef7c2bbf2
43 changed files with 1056 additions and 43 deletions

View File

@@ -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 targets as input for the rollout based on _target filter_ functionality.
- Selection of a _DistributionSet_. - Selection of a _DistributionSet_.
- Auto-splitting of the input target list into a defined number deployment groups. - 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. - 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. - 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. - Rollout progress monitoring for the entire rollout and the individual groups.

View File

@@ -340,6 +340,54 @@ public interface RolloutManagement {
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_HANDLE) @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_HANDLE)
void resumeRollout(long rolloutId); 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 * Starts a rollout which has been created. The rollout must be in
* {@link RolloutStatus#READY} state. The Rollout will be set into the * {@link RolloutStatus#READY} state. The Rollout will be set into the

View File

@@ -25,6 +25,17 @@ import org.eclipse.hawkbit.repository.model.TotalTargetCountStatus.Status;
*/ */
public interface Rollout extends NamedEntity { public interface Rollout extends NamedEntity {
/**
* Maximum length of author name.
*/
int APPROVED_BY_MAX_SIZE = 40;
/**
* Maximum length on comment regarding approval decision.
*/
int APPROVAL_REMARK_MAX_SIZE = 255;
/** /**
* @return <code>true</code> if the rollout is deleted and only kept for * @return <code>true</code> if the rollout is deleted and only kept for
* history purposes. * history purposes.
@@ -81,6 +92,16 @@ public interface Rollout extends NamedEntity {
*/ */
TotalTargetCountStatus getTotalTargetCountStatus(); 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. * State machine for rollout.
@@ -93,6 +114,16 @@ public interface Rollout extends NamedEntity {
*/ */
CREATING, CREATING,
/**
* Rollout needs to be approved.
*/
WAITING_FOR_APPROVAL,
/**
* Rollout approval is denied. Can not be started.
*/
APPROVAL_DENIED,
/** /**
* Rollout is ready to start. * Rollout is ready to start.
*/ */
@@ -154,4 +185,18 @@ public interface Rollout extends NamedEntity {
ERROR_STARTING; ERROR_STARTING;
} }
/**
* Enum that holds all possible approval workflow decisions.
*/
enum ApprovalDecision {
/**
* Representing an granted approval for a rollout.
*/
APPROVED,
/**
* Representing a rejected rollout.
*/
DENIED
}
} }

View File

@@ -114,6 +114,11 @@ public class TenantConfigurationProperties {
*/ */
public static final String ANONYMOUS_DOWNLOAD_MODE_ENABLED = "anonymous.download.enabled"; 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 * Repository on autoclose mode instead of canceling in case of new DS
* assignment over active actions. * assignment over active actions.

View File

@@ -59,12 +59,14 @@ public abstract class AbstractRolloutManagement implements RolloutManagement {
protected final LockRegistry lockRegistry; protected final LockRegistry lockRegistry;
protected final RolloutApprovalStrategy rolloutApprovalStrategy;
protected AbstractRolloutManagement(final TargetManagement targetManagement, protected AbstractRolloutManagement(final TargetManagement targetManagement,
final DeploymentManagement deploymentManagement, final RolloutGroupManagement rolloutGroupManagement, final DeploymentManagement deploymentManagement, final RolloutGroupManagement rolloutGroupManagement,
final DistributionSetManagement distributionSetManagement, final ApplicationContext context, final DistributionSetManagement distributionSetManagement, final ApplicationContext context,
final ApplicationEventPublisher eventPublisher, final VirtualPropertyReplacer virtualPropertyReplacer, final ApplicationEventPublisher eventPublisher, final VirtualPropertyReplacer virtualPropertyReplacer,
final PlatformTransactionManager txManager, final TenantAware tenantAware, final PlatformTransactionManager txManager, final TenantAware tenantAware,
final LockRegistry lockRegistry) { final LockRegistry lockRegistry, final RolloutApprovalStrategy rolloutApprovalStrategy) {
this.targetManagement = targetManagement; this.targetManagement = targetManagement;
this.deploymentManagement = deploymentManagement; this.deploymentManagement = deploymentManagement;
this.rolloutGroupManagement = rolloutGroupManagement; this.rolloutGroupManagement = rolloutGroupManagement;
@@ -75,6 +77,7 @@ public abstract class AbstractRolloutManagement implements RolloutManagement {
this.txManager = txManager; this.txManager = txManager;
this.tenantAware = tenantAware; this.tenantAware = tenantAware;
this.lockRegistry = lockRegistry; this.lockRegistry = lockRegistry;
this.rolloutApprovalStrategy = rolloutApprovalStrategy;
} }
protected Long runInNewTransaction(final String transactionName, final TransactionCallback<Long> action) { protected Long runInNewTransaction(final String transactionName, final TransactionCallback<Long> action) {

View File

@@ -0,0 +1,51 @@
/**
* Copyright (c) 2018 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository;
import org.eclipse.hawkbit.repository.model.Rollout;
/**
* This interface provides methods to enable plugging in of different strategies
* to handle rollout approval.
*/
public interface RolloutApprovalStrategy {
/**
* This method handles whether a rollout needs approval. Various factors may be
* important according to the implementation, e.g. user roles of the rollout
* creator, state of the system, ....
*
* @param rollout
* rollout to decide for whether approval is needed.
* @return true if the rollout according to this strategy needs approval.
*/
boolean isApprovalNeeded(Rollout rollout);
/**
* Depending on the implementation, creation of a approval task,
* notification,... inside or outside of hawkbit may be necessary.
* Implementations may also decide to provide an empty implementation for this
* method.
*
* @param rollout
* rollout to create approval task for.
*/
void onApprovalRequired(Rollout rollout);
/**
* Returns the user that made a decision to approve or deny the given rollout.
* Depending on the implementation this may be different to the current user eg.
* when the decision is made in an external system.
*
* @param rollout
* target rollout
* @return identifier of the user that decided on approval
*/
String getApprovalUser(Rollout rollout);
}

View File

@@ -71,4 +71,10 @@ hawkbit.server.tenant.configuration.anonymous-download-enabled.keyName=anonymous
hawkbit.server.tenant.configuration.anonymous-download-enabled.defaultValue=${hawkbit.server.download.anonymous.enabled} hawkbit.server.tenant.configuration.anonymous-download-enabled.defaultValue=${hawkbit.server.download.anonymous.enabled}
hawkbit.server.tenant.configuration.anonymous-download-enabled.dataType=java.lang.Boolean 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.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 # Default tenant configuration - END

View File

@@ -0,0 +1,85 @@
/**
* Copyright (c) 2018 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository.jpa;
import org.eclipse.hawkbit.im.authentication.SpPermission;
import org.eclipse.hawkbit.im.authentication.UserPrincipal;
import org.eclipse.hawkbit.repository.RolloutApprovalStrategy;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
/**
* Default implementation of {@link RolloutApprovalStrategy}. Decides whether
* approval is needed based on configuration of the tenant as well as the roles
* of the user who created the Rollout. Provides a no-operation implementation
* of {@link RolloutApprovalStrategy#onApprovalRequired(Rollout)}.
*/
public class DefaultRolloutApprovalStrategy implements RolloutApprovalStrategy {
private final UserDetailsService userDetailsService;
private final TenantConfigurationManagement tenantConfigurationManagement;
private final SystemSecurityContext systemSecurityContext;
DefaultRolloutApprovalStrategy(UserDetailsService userDetailsService,
TenantConfigurationManagement tenantConfigurationManagement,
SystemSecurityContext systemSecurityContext) {
this.userDetailsService = userDetailsService;
this.tenantConfigurationManagement = tenantConfigurationManagement;
this.systemSecurityContext = systemSecurityContext;
}
/**
* Returns true, if rollout approval is enabled and rollout creator doesn't have
* approval role.
*/
@Override
public boolean isApprovalNeeded(final Rollout rollout) {
final UserDetails userDetails = this.getActor(rollout);
final boolean approvalEnabled = this.tenantConfigurationManagement
.getConfigurationValue(TenantConfigurationKey.ROLLOUT_APPROVAL_ENABLED, Boolean.class).getValue();
return approvalEnabled && userDetails.getAuthorities().stream()
.noneMatch(authority -> SpPermission.APPROVE_ROLLOUT.equals(authority.getAuthority()));
}
private UserDetails getActor(Rollout rollout) {
final String actor = rollout.getLastModifiedBy() != null ? rollout.getLastModifiedBy() : rollout.getCreatedBy();
return systemSecurityContext.runAsSystem(() -> {
UserPrincipal userPrincipal = (UserPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if(userPrincipal.getUsername().equals(actor)) {
return userPrincipal;
} else {
return this.userDetailsService.loadUserByUsername(actor);
}
});
}
/***
* Per default do nothing.
*
* @param rollout
* rollout to create approval task for.
*/
@Override
public void onApprovalRequired(Rollout rollout) {
// do nothing per default, can be extended by further implementations.
}
@Override
public String getApprovalUser(Rollout rollout) {
return SecurityContextHolder.getContext().getAuthentication().getName();
}
}

View File

@@ -25,6 +25,7 @@ import org.eclipse.hawkbit.repository.AbstractRolloutManagement;
import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.DistributionSetManagement;
import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.QuotaManagement;
import org.eclipse.hawkbit.repository.RolloutApprovalStrategy;
import org.eclipse.hawkbit.repository.RolloutFields; import org.eclipse.hawkbit.repository.RolloutFields;
import org.eclipse.hawkbit.repository.RolloutGroupManagement; import org.eclipse.hawkbit.repository.RolloutGroupManagement;
import org.eclipse.hawkbit.repository.RolloutHelper; import org.eclipse.hawkbit.repository.RolloutHelper;
@@ -157,9 +158,9 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
final DistributionSetManagement distributionSetManagement, final ApplicationContext context, final DistributionSetManagement distributionSetManagement, final ApplicationContext context,
final ApplicationEventPublisher eventPublisher, final VirtualPropertyReplacer virtualPropertyReplacer, final ApplicationEventPublisher eventPublisher, final VirtualPropertyReplacer virtualPropertyReplacer,
final PlatformTransactionManager txManager, final TenantAware tenantAware, final LockRegistry lockRegistry, final PlatformTransactionManager txManager, final TenantAware tenantAware, final LockRegistry lockRegistry,
final Database database) { final Database database, final RolloutApprovalStrategy rolloutApprovalStrategy) {
super(targetManagement, deploymentManagement, rolloutGroupManagement, distributionSetManagement, context, super(targetManagement, deploymentManagement, rolloutGroupManagement, distributionSetManagement, context,
eventPublisher, virtualPropertyReplacer, txManager, tenantAware, lockRegistry); eventPublisher, virtualPropertyReplacer, txManager, tenantAware, lockRegistry, rolloutApprovalStrategy);
this.database = database; 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 // When all groups are ready the rollout status can be changed to be
// ready, too. // ready, too.
if (readyGroups == rolloutGroups.size()) { if (readyGroups == rolloutGroups.size()) {
LOGGER.debug("rollout {} creation done. Switch to READY.", rollout.getId()); if (!rolloutApprovalStrategy.isApprovalNeeded(rollout)) {
rollout.setStatus(RolloutStatus.READY); 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.setLastCheck(0);
rollout.setTotalTargets(totalTargets); rollout.setTotalTargets(totalTargets);
rolloutRepository.save(rollout); rolloutRepository.save(rollout);
@@ -451,6 +458,39 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
groups.stream().map(RolloutGroupCreate::build).collect(Collectors.toList()), baseFilter, totalTargets)); 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 @Override
@Transactional @Transactional
@Retryable(include = { @Retryable(include = {
@@ -964,7 +1004,6 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
final JpaRollout rollout = getRolloutAndThrowExceptionIfNotFound(update.getId()); final JpaRollout rollout = getRolloutAndThrowExceptionIfNotFound(update.getId());
checkIfDeleted(update.getId(), rollout.getStatus()); checkIfDeleted(update.getId(), rollout.getStatus());
update.getName().ifPresent(rollout::setName); update.getName().ifPresent(rollout::setName);
update.getDescription().ifPresent(rollout::setDescription); update.getDescription().ifPresent(rollout::setDescription);
update.getActionType().ifPresent(rollout::setActionType); update.getActionType().ifPresent(rollout::setActionType);
@@ -976,6 +1015,11 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
rollout.setDistributionSet(set); rollout.setDistributionSet(set);
}); });
if (rolloutApprovalStrategy.isApprovalNeeded(rollout)) {
rollout.setStatus(RolloutStatus.WAITING_FOR_APPROVAL);
rollout.setApprovalDecidedBy(null);
rollout.setApprovalRemark(null);
}
return rolloutRepository.save(rollout); 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 * Enforces the quota defining the maximum number of {@link Target}s per
* {@link RolloutGroup}. * {@link RolloutGroup}.
* *
* @param group * @param group
* The rollout group * The rollout group
* @param requested * @param requested
@@ -1081,7 +1125,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
/** /**
* Enforces the quota defining the maximum number of {@link Action}s per * Enforces the quota defining the maximum number of {@link Action}s per
* {@link Target}. * {@link Target}.
* *
* @param target * @param target
* The target * The target
* @param requested * @param requested

View File

@@ -26,6 +26,7 @@ import org.eclipse.hawkbit.repository.PropertiesQuotaManagement;
import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.QuotaManagement;
import org.eclipse.hawkbit.repository.RepositoryDefaultConfiguration; import org.eclipse.hawkbit.repository.RepositoryDefaultConfiguration;
import org.eclipse.hawkbit.repository.RepositoryProperties; import org.eclipse.hawkbit.repository.RepositoryProperties;
import org.eclipse.hawkbit.repository.RolloutApprovalStrategy;
import org.eclipse.hawkbit.repository.RolloutGroupManagement; import org.eclipse.hawkbit.repository.RolloutGroupManagement;
import org.eclipse.hawkbit.repository.RolloutManagement; import org.eclipse.hawkbit.repository.RolloutManagement;
import org.eclipse.hawkbit.repository.RolloutStatusCache; 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.orm.jpa.vendor.EclipseLinkJpaVendorAdapter;
import org.springframework.retry.annotation.EnableRetry; import org.springframework.retry.annotation.EnableRetry;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.jta.JtaTransactionManager; import org.springframework.transaction.jta.JtaTransactionManager;
@@ -471,7 +473,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
* to access quotas * to access quotas
* @param properties * @param properties
* JPA properties * JPA properties
* *
* @return a new {@link TargetFilterQueryManagement} * @return a new {@link TargetFilterQueryManagement}
*/ */
@Bean @Bean
@@ -559,10 +561,25 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
final DistributionSetManagement distributionSetManagement, final ApplicationContext context, final DistributionSetManagement distributionSetManagement, final ApplicationContext context,
final ApplicationEventPublisher eventPublisher, final VirtualPropertyReplacer virtualPropertyReplacer, final ApplicationEventPublisher eventPublisher, final VirtualPropertyReplacer virtualPropertyReplacer,
final PlatformTransactionManager txManager, final TenantAware tenantAware, final LockRegistry lockRegistry, final PlatformTransactionManager txManager, final TenantAware tenantAware, final LockRegistry lockRegistry,
final JpaProperties properties) { final JpaProperties properties, final RolloutApprovalStrategy rolloutApprovalStrategy) {
return new JpaRolloutManagement(targetManagement, deploymentManagement, rolloutGroupManagement, return new JpaRolloutManagement(targetManagement, deploymentManagement, rolloutGroupManagement,
distributionSetManagement, context, eventPublisher, virtualPropertyReplacer, txManager, tenantAware, distributionSetManagement, context, eventPublisher, virtualPropertyReplacer, txManager, tenantAware,
lockRegistry, properties.getDatabase()); lockRegistry, properties.getDatabase(), rolloutApprovalStrategy);
}
/**
* {@link DefaultRolloutApprovalStrategy} bean.
*
* @return a new {@link RolloutApprovalStrategy}
*/
@Bean
@ConditionalOnMissingBean
RolloutApprovalStrategy rolloutApprovalStrategy(final UserDetailsService userDetailsService,
final TenantConfigurationManagement tenantConfigurationManagement,
final SystemSecurityContext systemSecurityContext) {
return new DefaultRolloutApprovalStrategy(userDetailsService, tenantConfigurationManagement,
systemSecurityContext);
} }
/** /**

View File

@@ -88,7 +88,9 @@ public class JpaRollout extends AbstractJpaNamedEntity implements Rollout, Event
@ConversionValue(objectValue = "ERROR_CREATING", dataValue = "7"), @ConversionValue(objectValue = "ERROR_CREATING", dataValue = "7"),
@ConversionValue(objectValue = "ERROR_STARTING", dataValue = "8"), @ConversionValue(objectValue = "ERROR_STARTING", dataValue = "8"),
@ConversionValue(objectValue = "DELETING", dataValue = "9"), @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") @Convert("rolloutstatus")
@NotNull @NotNull
private RolloutStatus status = RolloutStatus.CREATING; private RolloutStatus status = RolloutStatus.CREATING;
@@ -120,6 +122,14 @@ public class JpaRollout extends AbstractJpaNamedEntity implements Rollout, Event
@Column(name = "start_at") @Column(name = "start_at")
private Long startAt; 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 @Transient
private transient TotalTargetCountStatus totalTargetCountStatus; private transient TotalTargetCountStatus totalTargetCountStatus;
@@ -271,4 +281,22 @@ public class JpaRollout extends AbstractJpaNamedEntity implements Rollout, Event
this.deleted = deleted; this.deleted = deleted;
} }
@Override
public String getApprovalDecidedBy() {
return approvalDecidedBy;
}
public void setApprovalDecidedBy(final String approvalDecidedBy) {
this.approvalDecidedBy = approvalDecidedBy;
}
@Override
public String getApprovalRemark() {
return approvalRemark;
}
public void setApprovalRemark(final String approvalRemark) {
this.approvalRemark = approvalRemark;
}
} }

View File

@@ -0,0 +1,2 @@
ALTER TABLE sp_rollout ADD column approval_decided_by varchar(40);
ALTER TABLE sp_rollout ADD column approval_remark varchar(255);

View File

@@ -0,0 +1,2 @@
ALTER TABLE sp_rollout ADD column approval_decided_by varchar(40);
ALTER TABLE sp_rollout ADD column approval_remark varchar(255);

View File

@@ -0,0 +1,2 @@
ALTER TABLE sp_rollout ADD column approval_decided_by varchar(40);
ALTER TABLE sp_rollout ADD column approval_remark varchar(255);

View File

@@ -0,0 +1,2 @@
ALTER TABLE sp_rollout ADD column approval_decided_by varchar(40);
ALTER TABLE sp_rollout ADD column approval_remark varchar(255);

View File

@@ -27,6 +27,7 @@ import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetTag; import org.eclipse.hawkbit.repository.model.TargetTag;
import org.eclipse.hawkbit.repository.model.TargetTagAssignmentResult; import org.eclipse.hawkbit.repository.model.TargetTagAssignmentResult;
import org.eclipse.hawkbit.repository.test.util.AbstractIntegrationTest; 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.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.boot.test.SpringApplicationConfiguration;
@@ -95,6 +96,9 @@ public abstract class AbstractJpaIntegrationTest extends AbstractIntegrationTest
@Autowired @Autowired
protected TenantConfigurationProperties tenantConfigurationProperties; protected TenantConfigurationProperties tenantConfigurationProperties;
@Autowired
protected RolloutTestApprovalStrategy approvalStrategy;
@Transactional(readOnly = true) @Transactional(readOnly = true)
protected List<Action> findActionsByRolloutAndStatus(final Rollout rollout, final Action.Status actionStatus) { protected List<Action> findActionsByRolloutAndStatus(final Rollout rollout, final Action.Status actionStatus) {
return Lists.newArrayList(actionRepository.findByRolloutIdAndStatus(PAGE, rollout.getId(), actionStatus)); return Lists.newArrayList(actionRepository.findByRolloutIdAndStatus(PAGE, rollout.getId(), actionStatus));

View File

@@ -68,12 +68,17 @@ import org.eclipse.hawkbit.repository.model.TotalTargetCountStatus;
import org.eclipse.hawkbit.repository.test.matcher.Expect; import org.eclipse.hawkbit.repository.test.matcher.Expect;
import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents; import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey; import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
import org.junit.After;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Slice; import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction; 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.Description;
import ru.yandex.qatools.allure.annotations.Features; import ru.yandex.qatools.allure.annotations.Features;
import ru.yandex.qatools.allure.annotations.Step; import ru.yandex.qatools.allure.annotations.Step;
@@ -87,6 +92,12 @@ import ru.yandex.qatools.allure.annotations.Title;
@Stories("Rollout Management") @Stories("Rollout Management")
public class RolloutManagementTest extends AbstractJpaIntegrationTest { public class RolloutManagementTest extends AbstractJpaIntegrationTest {
@Before
@After
public void reset() {
this.approvalStrategy.setApprovalNeeded(false);
}
@Test @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)") @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() { 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 @Test
@ExpectEvents({ @Expect(type = RolloutDeletedEvent.class, count = 1), @ExpectEvents({ @Expect(type = RolloutDeletedEvent.class, count = 1),
@Expect(type = DistributionSetCreatedEvent.class, count = 1), @Expect(type = DistributionSetCreatedEvent.class, count = 1),

View File

@@ -23,11 +23,13 @@ import org.eclipse.hawkbit.cache.DefaultDownloadIdCache;
import org.eclipse.hawkbit.cache.DownloadIdCache; import org.eclipse.hawkbit.cache.DownloadIdCache;
import org.eclipse.hawkbit.cache.TenantAwareCacheManager; import org.eclipse.hawkbit.cache.TenantAwareCacheManager;
import org.eclipse.hawkbit.event.BusProtoStuffMessageConverter; import org.eclipse.hawkbit.event.BusProtoStuffMessageConverter;
import org.eclipse.hawkbit.repository.RolloutApprovalStrategy;
import org.eclipse.hawkbit.repository.RolloutStatusCache; import org.eclipse.hawkbit.repository.RolloutStatusCache;
import org.eclipse.hawkbit.repository.event.ApplicationEventFilter; import org.eclipse.hawkbit.repository.event.ApplicationEventFilter;
import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder; import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer; import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver; 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.repository.test.util.TestdataFactory;
import org.eclipse.hawkbit.security.DdiSecurityProperties; import org.eclipse.hawkbit.security.DdiSecurityProperties;
import org.eclipse.hawkbit.security.HawkbitSecurityProperties; import org.eclipse.hawkbit.security.HawkbitSecurityProperties;
@@ -128,7 +130,6 @@ public class TestConfiguration implements AsyncConfigurer {
TenantAwareCacheManager cacheManager() { TenantAwareCacheManager cacheManager() {
return new TenantAwareCacheManager(new GuavaCacheManager(), tenantAware()); return new TenantAwareCacheManager(new GuavaCacheManager(), tenantAware());
} }
/** /**
* Bean for the download id cache. * Bean for the download id cache.
* *
@@ -214,6 +215,11 @@ public class TestConfiguration implements AsyncConfigurer {
return serviceMatcher; return serviceMatcher;
} }
@Bean
RolloutApprovalStrategy rolloutApprovalStrategy() {
return new RolloutTestApprovalStrategy();
}
/** /**
* *
* @return the protostuff io message converter * @return the protostuff io message converter

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2018 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository.test.util;
import org.eclipse.hawkbit.repository.RolloutApprovalStrategy;
import org.eclipse.hawkbit.repository.model.Rollout;
/**
* Provides an approval strategy that can be manipulated by setting the {link
* {@link #approvalNeeded}} flag used for testing.
*/
public class RolloutTestApprovalStrategy implements RolloutApprovalStrategy {
private boolean approvalNeeded = false;
@Override
public boolean isApprovalNeeded(Rollout rollout) {
return approvalNeeded;
}
public void setApprovalNeeded(boolean approvalNeeded) {
this.approvalNeeded = approvalNeeded;
}
@Override
public void onApprovalRequired(Rollout rollout) {
// do nothing, as no action is needed when testing
}
@Override
public String getApprovalUser(Rollout rollout) {
return null;
}
}

View File

@@ -81,6 +81,36 @@ public interface MgmtRolloutRestApi {
MediaType.APPLICATION_JSON_VALUE }) MediaType.APPLICATION_JSON_VALUE })
ResponseEntity<MgmtRolloutResponseBody> create(MgmtRolloutRestRequestBody rolloutRequestBody); ResponseEntity<MgmtRolloutResponseBody> 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<Void> 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<Void> deny(@PathVariable("rolloutId") Long rolloutId,
@RequestParam(value = "remark", required = false) String remark);
/** /**
* Handles the POST request for starting a rollout. * Handles the POST request for starting a rollout.
* *

View File

@@ -88,6 +88,8 @@ final class MgmtRolloutMapper {
body.add(linkTo(methodOn(MgmtRolloutRestApi.class).start(rollout.getId())).withRel("start")); 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).pause(rollout.getId())).withRel("pause"));
body.add(linkTo(methodOn(MgmtRolloutRestApi.class).resume(rollout.getId())).withRel("resume")); 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(), body.add(linkTo(methodOn(MgmtRolloutRestApi.class).getRolloutGroups(rollout.getId(),
MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET_VALUE, MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET_VALUE,
MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT_VALUE, null, null)).withRel("groups")); MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT_VALUE, null, null)).withRel("groups"));

View File

@@ -129,6 +129,18 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi {
return ResponseEntity.status(HttpStatus.CREATED).body(MgmtRolloutMapper.toResponseRollout(rollout, true)); return ResponseEntity.status(HttpStatus.CREATED).body(MgmtRolloutMapper.toResponseRollout(rollout, true));
} }
@Override
public ResponseEntity<Void> approve(@PathVariable("rolloutId") final Long rolloutId, final String remark) {
rolloutManagement.approveOrDeny(rolloutId, Rollout.ApprovalDecision.APPROVED, remark);
return ResponseEntity.ok().build();
}
@Override
public ResponseEntity<Void> deny(@PathVariable("rolloutId") final Long rolloutId, final String remark) {
rolloutManagement.approveOrDeny(rolloutId, Rollout.ApprovalDecision.DENIED, remark);
return ResponseEntity.ok().build();
}
@Override @Override
public ResponseEntity<Void> start(@PathVariable("rolloutId") final Long rolloutId) { public ResponseEntity<Void> start(@PathVariable("rolloutId") final Long rolloutId) {
this.rolloutManagement.start(rolloutId); this.rolloutManagement.start(rolloutId);

View File

@@ -179,6 +179,81 @@ include::../errors/415.adoc[]
include::../errors/429.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 == POST /rest/v1/rollouts/{rolloutId}/start
=== Implementation Notes === Implementation Notes

View File

@@ -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_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_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_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_LINKS_GROUPS = "Link to retrieve the groups a rollout";
public static final String ROLLOUT_START_ASYNC = "Start the rollout asynchronous"; public static final String ROLLOUT_START_ASYNC = "Start the rollout asynchronous";

View File

@@ -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.RolloutGroup.RolloutGroupSuccessCondition;
import org.eclipse.hawkbit.repository.model.RolloutGroupConditionBuilder; import org.eclipse.hawkbit.repository.model.RolloutGroupConditionBuilder;
import org.eclipse.hawkbit.repository.model.RolloutGroupConditions; 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.AbstractApiRestDocumentation;
import org.eclipse.hawkbit.rest.documentation.ApiModelPropertiesGeneric; import org.eclipse.hawkbit.rest.documentation.ApiModelPropertiesGeneric;
import org.eclipse.hawkbit.rest.documentation.DocumenationResponseFieldsSnippet; 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.eclipse.hawkbit.rest.util.MockMvcResultPrinter;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.hateoas.MediaTypes; import org.springframework.hateoas.MediaTypes;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -62,12 +64,16 @@ import ru.yandex.qatools.allure.annotations.Stories;
@Stories("Rollout Resource") @Stories("Rollout Resource")
public class RolloutResourceDocumentationTest extends AbstractApiRestDocumentation { public class RolloutResourceDocumentationTest extends AbstractApiRestDocumentation {
@Autowired
private RolloutTestApprovalStrategy approvalStrategy;
@Override @Override
@Before @Before
public void setUp() { public void setUp() {
this.resourceName = "rollouts"; this.resourceName = "rollouts";
super.setUp(); super.setUp();
arrayPrefix = "content[]."; arrayPrefix = "content[].";
approvalStrategy.setApprovalNeeded(false);
} }
@Test @Test
@@ -127,7 +133,6 @@ public class RolloutResourceDocumentationTest extends AbstractApiRestDocumentati
allFieldDescriptor.add( allFieldDescriptor.add(
fieldWithPath(arrayPrefix + "totalTargets").description(MgmtApiModelProperties.ROLLOUT_TOTAL_TARGETS)); fieldWithPath(arrayPrefix + "totalTargets").description(MgmtApiModelProperties.ROLLOUT_TOTAL_TARGETS));
allFieldDescriptor.add(fieldWithPath(arrayPrefix + "_links.self").ignored()); allFieldDescriptor.add(fieldWithPath(arrayPrefix + "_links.self").ignored());
if (withDetails) { if (withDetails) {
allFieldDescriptor.add(fieldWithPath(arrayPrefix + "totalTargetsPerStatus") allFieldDescriptor.add(fieldWithPath(arrayPrefix + "totalTargetsPerStatus")
.description(MgmtApiModelProperties.ROLLOUT_TOTAL_TARGETS_PER_STATUS)); .description(MgmtApiModelProperties.ROLLOUT_TOTAL_TARGETS_PER_STATUS));
@@ -139,6 +144,10 @@ public class RolloutResourceDocumentationTest extends AbstractApiRestDocumentati
.description(MgmtApiModelProperties.ROLLOUT_LINKS_RESUME)); .description(MgmtApiModelProperties.ROLLOUT_LINKS_RESUME));
allFieldDescriptor.add(fieldWithPath(arrayPrefix + "_links.groups") allFieldDescriptor.add(fieldWithPath(arrayPrefix + "_links.groups")
.description(MgmtApiModelProperties.ROLLOUT_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); return new DocumenationResponseFieldsSnippet(allFieldDescriptor);
@@ -386,6 +395,29 @@ public class RolloutResourceDocumentationTest extends AbstractApiRestDocumentati
pathParameters(parameterWithName("rolloutId").description(ApiModelPropertiesGeneric.ITEM_ID)))); 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 @Test
@Description("Handles the GET request of retrieving the deploy groups of a rollout. Required Permission: " @Description("Handles the GET request of retrieving the deploy groups of a rollout. Required Permission: "
+ SpPermission.READ_ROLLOUT) + SpPermission.READ_ROLLOUT)

View File

@@ -80,6 +80,8 @@ public class TenantResourceDocumentationTest extends AbstractApiRestDocumentatio
"if the anonymous download mode is enabled."); "if the anonymous download mode is enabled.");
CONFIG_ITEM_DESCRIPTIONS.put(TenantConfigurationKey.REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED, CONFIG_ITEM_DESCRIPTIONS.put(TenantConfigurationKey.REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED,
"if autoclose running actions with new Distribution Set assignment is 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 @Autowired

View File

@@ -42,6 +42,7 @@ spring.http.multipart.max-request-size=-1
# UI help links # UI help links
hawkbit.server.ui.links.documentation.root=https://www.eclipse.org/hawkbit/documentation/overview/introduction.html 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.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.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 hawkbit.server.ui.links.documentation.distribution-view=https://www.eclipse.org/hawkbit/documentation/interfaces/management-ui.html

View File

@@ -145,6 +145,11 @@ public final class SpPermission {
*/ */
public static final String HANDLE_ROLLOUT = "HANDLE_ROLLOUT"; 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() { private SpPermission() {
// Constants only // Constants only
} }
@@ -389,6 +394,14 @@ public final class SpPermission {
public static final String HAS_AUTH_ROLLOUT_MANAGEMENT_HANDLE = HAS_AUTH_PREFIX + HANDLE_ROLLOUT public static final String HAS_AUTH_ROLLOUT_MANAGEMENT_HANDLE = HAS_AUTH_PREFIX + HANDLE_ROLLOUT
+ HAS_AUTH_SUFFIX + HAS_AUTH_OR + IS_SYSTEM_CODE; + 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 * Spring security eval hasAuthority expression to check if spring
* context contains {@link SpPermission#UPDATE_ROLLOUT} or * context contains {@link SpPermission#UPDATE_ROLLOUT} or

View File

@@ -31,7 +31,7 @@ public final class PermissionTest {
@Test @Test
@Description("Verify the get permission function") @Description("Verify the get permission function")
public void testGetPermissions() { public void testGetPermissions() {
final int allPermission = 17; final int allPermission = 18;
final Collection<String> allAuthorities = SpPermission.getAllAuthorities(); final Collection<String> allAuthorities = SpPermission.getAllAuthorities();
final List<GrantedAuthority> allAuthoritiesList = PermissionUtils.createAllAuthorityList(); final List<GrantedAuthority> allAuthoritiesList = PermissionUtils.createAllAuthorityList();
assertThat(allAuthorities).hasSize(allPermission); assertThat(allAuthorities).hasSize(allPermission);

View File

@@ -151,4 +151,12 @@ public class SpPermissionChecker implements Serializable {
public boolean hasRolloutTargetsReadPermission() { public boolean hasRolloutTargetsReadPermission() {
return hasTargetReadPermission() && permissionService.hasPermission(SpPermission.READ_ROLLOUT); return hasTargetReadPermission() && permissionService.hasPermission(SpPermission.READ_ROLLOUT);
} }
/**
*
* @return <code>true</code> if rollout can be approved by the user.
*/
public boolean hasRolloutApprovalPermission() {
return hasRolloutReadPermission() && permissionService.hasPermission(SpPermission.APPROVE_ROLLOUT);
}
} }

View File

@@ -137,6 +137,11 @@ public class UiProperties implements Serializable {
*/ */
private String security = ""; private String security = "";
/**
* Link to rollout related documentation.
*/
private String rollout = "";
/** /**
* Link to target filter view. * Link to target filter view.
*/ */
@@ -167,6 +172,10 @@ public class UiProperties implements Serializable {
return security; return security;
} }
public String getRollout() {
return rollout;
}
public String getSystemConfigurationView() { public String getSystemConfigurationView() {
return systemConfigurationView; return systemConfigurationView;
} }
@@ -203,6 +212,10 @@ public class UiProperties implements Serializable {
this.security = security; this.security = security;
} }
public void setRollout(final String rollout) {
this.rollout = rollout;
}
public void setSystemConfigurationView(final String systemConfigurationView) { public void setSystemConfigurationView(final String systemConfigurationView) {
this.systemConfigurationView = systemConfigurationView; this.systemConfigurationView = systemConfigurationView;
} }

View File

@@ -19,6 +19,7 @@ import org.eclipse.hawkbit.repository.RolloutGroupManagement;
import org.eclipse.hawkbit.repository.RolloutManagement; import org.eclipse.hawkbit.repository.RolloutManagement;
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
import org.eclipse.hawkbit.repository.TargetManagement; 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;
import org.eclipse.hawkbit.ui.AbstractHawkbitUI; import org.eclipse.hawkbit.ui.AbstractHawkbitUI;
import org.eclipse.hawkbit.ui.SpPermissionChecker; import org.eclipse.hawkbit.ui.SpPermissionChecker;
@@ -69,16 +70,17 @@ public class RolloutView extends VerticalLayout implements View {
@Autowired @Autowired
RolloutView(final SpPermissionChecker permissionChecker, final RolloutUIState rolloutUIState, RolloutView(final SpPermissionChecker permissionChecker, final RolloutUIState rolloutUIState,
final UIEventBus eventBus, final RolloutManagement rolloutManagement, final UIEventBus eventBus, final RolloutManagement rolloutManagement,
final RolloutGroupManagement rolloutGroupManagement, final TargetManagement targetManagement, final RolloutGroupManagement rolloutGroupManagement, final TargetManagement targetManagement,
final UINotification uiNotification, final UiProperties uiProperties, final EntityFactory entityFactory, final UINotification uiNotification, final UiProperties uiProperties, final EntityFactory entityFactory,
final VaadinMessageSource i18n, final TargetFilterQueryManagement targetFilterQueryManagement, final VaadinMessageSource i18n, final TargetFilterQueryManagement targetFilterQueryManagement,
final QuotaManagement quotaManagement) { final QuotaManagement quotaManagement,
final TenantConfigurationManagement tenantConfigManagement) {
this.permChecker = permissionChecker; this.permChecker = permissionChecker;
this.rolloutManagement = rolloutManagement; this.rolloutManagement = rolloutManagement;
this.rolloutListView = new RolloutListView(permissionChecker, rolloutUIState, eventBus, rolloutManagement, this.rolloutListView = new RolloutListView(permissionChecker, rolloutUIState, eventBus, rolloutManagement,
targetManagement, uiNotification, uiProperties, entityFactory, i18n, targetFilterQueryManagement, targetManagement, uiNotification, uiProperties, entityFactory, i18n, targetFilterQueryManagement,
rolloutGroupManagement, quotaManagement); rolloutGroupManagement, quotaManagement, tenantConfigManagement);
this.rolloutGroupsListView = new RolloutGroupsListView(i18n, eventBus, rolloutGroupManagement, rolloutUIState, this.rolloutGroupsListView = new RolloutGroupsListView(i18n, eventBus, rolloutGroupManagement, rolloutUIState,
permissionChecker); permissionChecker);
this.rolloutGroupTargetsListView = new RolloutGroupTargetsListView(eventBus, i18n, rolloutUIState); this.rolloutGroupTargetsListView = new RolloutGroupTargetsListView(eventBus, i18n, rolloutUIState);

View File

@@ -78,9 +78,11 @@ import com.vaadin.data.util.converter.StringToIntegerConverter;
import com.vaadin.data.validator.IntegerRangeValidator; import com.vaadin.data.validator.IntegerRangeValidator;
import com.vaadin.data.validator.LongRangeValidator; import com.vaadin.data.validator.LongRangeValidator;
import com.vaadin.data.validator.NullValidator; import com.vaadin.data.validator.NullValidator;
import com.vaadin.server.FontAwesome;
import com.vaadin.shared.ui.label.ContentMode; import com.vaadin.shared.ui.label.ContentMode;
import com.vaadin.ui.ComboBox; import com.vaadin.ui.ComboBox;
import com.vaadin.ui.GridLayout; import com.vaadin.ui.GridLayout;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Label; import com.vaadin.ui.Label;
import com.vaadin.ui.OptionGroup; import com.vaadin.ui.OptionGroup;
import com.vaadin.ui.TabSheet; 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 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 ActionTypeOptionGroupLayout actionTypeOptionGroupLayout;
private final AutoStartOptionGroupLayout autoStartOptionGroupLayout; private final AutoStartOptionGroupLayout autoStartOptionGroupLayout;
@@ -150,6 +156,10 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
private OptionGroup errorThresholdOptionGroup; private OptionGroup errorThresholdOptionGroup;
private Label approvalLabel;
private HorizontalLayout approvalButtonsLayout;
private CommonDialogWindow window; private CommonDialogWindow window;
private boolean editRolloutEnabled; private boolean editRolloutEnabled;
@@ -166,6 +176,10 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
private GroupsLegendLayout groupsLegendLayout; private GroupsLegendLayout groupsLegendLayout;
private OptionGroup approveButtonsGroup;
private TextField approvalRemarkField;
private final transient RolloutGroupConditions defaultRolloutGroupConditions; private final transient RolloutGroupConditions defaultRolloutGroupConditions;
private final NullValidator nullValidator = new NullValidator(null, false); private final NullValidator nullValidator = new NullValidator(null, false);
@@ -213,6 +227,11 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
if (editRolloutEnabled) { if (editRolloutEnabled) {
editRollout(); 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; return;
} }
createRollout(); createRollout();
@@ -413,6 +432,12 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
rollout = null; rollout = null;
groupsDefinitionTabs.setVisible(true); groupsDefinitionTabs.setVisible(true);
groupsDefinitionTabs.setSelectedTab(0); groupsDefinitionTabs.setSelectedTab(0);
approvalLabel.setVisible(false);
approvalButtonsLayout.setVisible(false);
approveButtonsGroup.clear();
approvalRemarkField.clear();
approveButtonsGroup.removeAllValidators();
} }
private void addGroupsDefinitionTabs() { private void addGroupsDefinitionTabs() {
@@ -443,7 +468,7 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
setSpacing(true); setSpacing(true);
setSizeUndefined(); setSizeUndefined();
setRows(7); setRows(8);
setColumns(4); setColumns(4);
setStyleName("marginTop"); setStyleName("marginTop");
setColumnExpandRatio(3, 1); setColumnExpandRatio(3, 1);
@@ -477,6 +502,9 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
addComponent(groupsDefinitionTabs, 0, 6, 3, 6); addComponent(groupsDefinitionTabs, 0, 6, 3, 6);
addComponent(approvalLabel, 0, 7);
addComponent(approvalButtonsLayout, 1, 7, 3, 7);
rolloutName.focus(); rolloutName.focus();
} }
@@ -541,6 +569,8 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
groupsLegendLayout = new GroupsLegendLayout(i18n); groupsLegendLayout = new GroupsLegendLayout(i18n);
approvalLabel = getLabel("label.approval.decision");
approvalButtonsLayout = createApprovalLayout();
} }
private void displayValidationStatus(final DefineGroupsLayout.ValidationStatus status) { private void displayValidationStatus(final DefineGroupsLayout.ValidationStatus status) {
@@ -610,6 +640,29 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
return layout; 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() { private static Label createCountLabel() {
final Label groupSize = new LabelBuilder().visible(false).name("").buildLabel(); final Label groupSize = new LabelBuilder().visible(false).name("").buildLabel();
groupSize.addStyleName(ValoTheme.LABEL_TINY + " " + "rollout-target-count-message"); groupSize.addStyleName(ValoTheme.LABEL_TINY + " " + "rollout-target-count-message");
@@ -966,6 +1019,13 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
if (rollout.getStatus() != Rollout.RolloutStatus.READY) { if (rollout.getStatus() != Rollout.RolloutStatus.READY) {
disableRequiredFieldsOnEdit(); disableRequiredFieldsOnEdit();
} }
if (rollout.getStatus() == Rollout.RolloutStatus.WAITING_FOR_APPROVAL) {
approvalButtonsLayout.setVisible(true);
approveButtonsGroup.addValidator(nullValidator);
approvalLabel.setVisible(true);
}
rolloutName.setValue(rollout.getName()); rolloutName.setValue(rollout.getName());
groupsDefinitionTabs.setVisible(false); groupsDefinitionTabs.setVisible(false);

View File

@@ -43,6 +43,8 @@ public class ProxyRollout {
private long forcedTime; private long forcedTime;
private RolloutStatus status; private RolloutStatus status;
private TotalTargetCountStatus totalTargetCountStatus; private TotalTargetCountStatus totalTargetCountStatus;
private String approvalDecidedBy;
private String approvalRemark;
public RolloutRendererData getRolloutRendererData() { public RolloutRendererData getRolloutRendererData() {
return rolloutRendererData; return rolloutRendererData;
@@ -217,4 +219,20 @@ public class ProxyRollout {
public void setTotalTargetCountStatus(final TotalTargetCountStatus totalTargetCountStatus) { public void setTotalTargetCountStatus(final TotalTargetCountStatus totalTargetCountStatus) {
this.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;
}
} }

View File

@@ -124,6 +124,8 @@ public class RolloutBeanQuery extends AbstractBeanQuery<ProxyRollout> {
final TotalTargetCountStatus totalTargetCountActionStatus = rollout.getTotalTargetCountStatus(); final TotalTargetCountStatus totalTargetCountActionStatus = rollout.getTotalTargetCountStatus();
proxyRollout.setTotalTargetCountStatus(totalTargetCountActionStatus); proxyRollout.setTotalTargetCountStatus(totalTargetCountActionStatus);
proxyRollout.setTotalTargetsCount(String.valueOf(rollout.getTotalTargets())); proxyRollout.setTotalTargetsCount(String.valueOf(rollout.getTotalTargets()));
proxyRollout.setApprovalDecidedBy(rollout.getApprovalDecidedBy());
proxyRollout.setApprovalRemark(rollout.getApprovalRemark());
return proxyRollout; return proxyRollout;
} }

View File

@@ -8,9 +8,11 @@
*/ */
package org.eclipse.hawkbit.ui.rollout.rollout; 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 static org.eclipse.hawkbit.ui.rollout.DistributionBarHelper.getTooltip;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.List; import java.util.List;
import java.util.Locale; 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.RolloutManagement;
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
import org.eclipse.hawkbit.repository.TargetManagement; 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;
import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus; import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus;
import org.eclipse.hawkbit.repository.model.TotalTargetCountStatus; import org.eclipse.hawkbit.repository.model.TotalTargetCountStatus;
@@ -77,6 +80,7 @@ public class RolloutListGrid extends AbstractGrid<LazyQueryContainer> {
private static final String ROLLOUT_RENDERER_DATA = "rolloutRendererData"; 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_RUN = "run";
private static final String VIRT_PROP_PAUSE = "pause"; private static final String VIRT_PROP_PAUSE = "pause";
private static final String VIRT_PROP_UPDATE = "update"; private static final String VIRT_PROP_UPDATE = "update";
@@ -87,6 +91,8 @@ public class RolloutListGrid extends AbstractGrid<LazyQueryContainer> {
private final transient RolloutGroupManagement rolloutGroupManagement; private final transient RolloutGroupManagement rolloutGroupManagement;
private final transient TenantConfigurationManagement tenantConfigManagement;
private final AddUpdateRolloutWindowLayout addUpdateRolloutWindow; private final AddUpdateRolloutWindowLayout addUpdateRolloutWindow;
private final UINotification uiNotification; private final UINotification uiNotification;
@@ -95,7 +101,8 @@ public class RolloutListGrid extends AbstractGrid<LazyQueryContainer> {
private static final List<RolloutStatus> DELETE_COPY_BUTTON_ENABLED = Arrays.asList(RolloutStatus.CREATING, private static final List<RolloutStatus> DELETE_COPY_BUTTON_ENABLED = Arrays.asList(RolloutStatus.CREATING,
RolloutStatus.ERROR_CREATING, RolloutStatus.ERROR_STARTING, RolloutStatus.PAUSED, RolloutStatus.READY, 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<RolloutStatus> UPDATE_BUTTON_ENABLED = Arrays.asList(RolloutStatus.CREATING, private static final List<RolloutStatus> UPDATE_BUTTON_ENABLED = Arrays.asList(RolloutStatus.CREATING,
RolloutStatus.ERROR_CREATING, RolloutStatus.ERROR_STARTING, RolloutStatus.PAUSED, RolloutStatus.READY, RolloutStatus.ERROR_CREATING, RolloutStatus.ERROR_STARTING, RolloutStatus.PAUSED, RolloutStatus.READY,
@@ -106,11 +113,14 @@ public class RolloutListGrid extends AbstractGrid<LazyQueryContainer> {
private static final List<RolloutStatus> RUN_BUTTON_ENABLED = Arrays.asList(RolloutStatus.READY, private static final List<RolloutStatus> RUN_BUTTON_ENABLED = Arrays.asList(RolloutStatus.READY,
RolloutStatus.PAUSED); RolloutStatus.PAUSED);
private static final List<RolloutStatus> APPROVE_BUTTON_ENABLED = Collections.singletonList(RolloutStatus.WAITING_FOR_APPROVAL);
private static final Map<RolloutStatus, StatusFontIcon> statusIconMap = new EnumMap<>(RolloutStatus.class); private static final Map<RolloutStatus, StatusFontIcon> statusIconMap = new EnumMap<>(RolloutStatus.class);
private static final List<Object> HIDDEN_COLUMNS = Arrays.asList(SPUILabelDefinitions.VAR_CREATED_DATE, private static final List<Object> HIDDEN_COLUMNS = Arrays.asList(SPUILabelDefinitions.VAR_CREATED_DATE,
SPUILabelDefinitions.VAR_CREATED_USER, SPUILabelDefinitions.VAR_MODIFIED_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 { static {
statusIconMap.put(RolloutStatus.FINISHED, statusIconMap.put(RolloutStatus.FINISHED,
@@ -118,6 +128,10 @@ public class RolloutListGrid extends AbstractGrid<LazyQueryContainer> {
statusIconMap.put(RolloutStatus.PAUSED, statusIconMap.put(RolloutStatus.PAUSED,
new StatusFontIcon(FontAwesome.PAUSE, SPUIStyleDefinitions.STATUS_ICON_BLUE)); new StatusFontIcon(FontAwesome.PAUSE, SPUIStyleDefinitions.STATUS_ICON_BLUE));
statusIconMap.put(RolloutStatus.RUNNING, new StatusFontIcon(null, SPUIStyleDefinitions.STATUS_SPINNER_YELLOW)); 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, statusIconMap.put(RolloutStatus.READY,
new StatusFontIcon(FontAwesome.DOT_CIRCLE_O, SPUIStyleDefinitions.STATUS_ICON_LIGHT_BLUE)); new StatusFontIcon(FontAwesome.DOT_CIRCLE_O, SPUIStyleDefinitions.STATUS_ICON_LIGHT_BLUE));
statusIconMap.put(RolloutStatus.STOPPED, statusIconMap.put(RolloutStatus.STOPPED,
@@ -132,14 +146,17 @@ public class RolloutListGrid extends AbstractGrid<LazyQueryContainer> {
} }
RolloutListGrid(final VaadinMessageSource i18n, final UIEventBus eventBus, RolloutListGrid(final VaadinMessageSource i18n, final UIEventBus eventBus,
final RolloutManagement rolloutManagement, final UINotification uiNotification, final RolloutManagement rolloutManagement, final UINotification uiNotification,
final RolloutUIState rolloutUIState, final SpPermissionChecker permissionChecker, final RolloutUIState rolloutUIState, final SpPermissionChecker permissionChecker,
final TargetManagement targetManagement, final EntityFactory entityFactory, final UiProperties uiProperties, final TargetManagement targetManagement, final EntityFactory entityFactory,
final TargetFilterQueryManagement targetFilterQueryManagement, final UiProperties uiProperties,
final RolloutGroupManagement rolloutGroupManagement, final QuotaManagement quotaManagement) { final TargetFilterQueryManagement targetFilterQueryManagement,
final RolloutGroupManagement rolloutGroupManagement, final QuotaManagement quotaManagement,
final TenantConfigurationManagement tenantConfigManagement) {
super(i18n, eventBus, permissionChecker); super(i18n, eventBus, permissionChecker);
this.rolloutManagement = rolloutManagement; this.rolloutManagement = rolloutManagement;
this.rolloutGroupManagement = rolloutGroupManagement; this.rolloutGroupManagement = rolloutGroupManagement;
this.tenantConfigManagement = tenantConfigManagement;
this.addUpdateRolloutWindow = new AddUpdateRolloutWindowLayout(rolloutManagement, targetManagement, this.addUpdateRolloutWindow = new AddUpdateRolloutWindowLayout(rolloutManagement, targetManagement,
uiNotification, uiProperties, entityFactory, i18n, eventBus, targetFilterQueryManagement, uiNotification, uiProperties, entityFactory, i18n, eventBus, targetFilterQueryManagement,
rolloutGroupManagement, quotaManagement); rolloutGroupManagement, quotaManagement);
@@ -250,7 +267,10 @@ public class RolloutListGrid extends AbstractGrid<LazyQueryContainer> {
false); false);
rolloutGridContainer.addContainerProperty(SPUILabelDefinitions.VAR_CREATED_DATE, String.class, null, false, rolloutGridContainer.addContainerProperty(SPUILabelDefinitions.VAR_CREATED_DATE, String.class, null, false,
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, rolloutGridContainer.addContainerProperty(SPUILabelDefinitions.VAR_MODIFIED_DATE, String.class, null, false,
false); false);
rolloutGridContainer.addContainerProperty(SPUILabelDefinitions.VAR_CREATED_USER, String.class, null, false, rolloutGridContainer.addContainerProperty(SPUILabelDefinitions.VAR_CREATED_USER, String.class, null, false,
@@ -289,6 +309,9 @@ public class RolloutListGrid extends AbstractGrid<LazyQueryContainer> {
getColumn(VIRT_PROP_RUN).setMinimumWidth(25); getColumn(VIRT_PROP_RUN).setMinimumWidth(25);
getColumn(VIRT_PROP_RUN).setMaximumWidth(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).setMinimumWidth(25);
getColumn(VIRT_PROP_PAUSE).setMaximumWidth(25); getColumn(VIRT_PROP_PAUSE).setMaximumWidth(25);
@@ -315,12 +338,15 @@ public class RolloutListGrid extends AbstractGrid<LazyQueryContainer> {
getColumn(SPUILabelDefinitions.VAR_CREATED_USER).setHeaderCaption(i18n.getMessage("header.createdBy")); getColumn(SPUILabelDefinitions.VAR_CREATED_USER).setHeaderCaption(i18n.getMessage("header.createdBy"));
getColumn(SPUILabelDefinitions.VAR_MODIFIED_DATE).setHeaderCaption(i18n.getMessage("header.modifiedDate")); getColumn(SPUILabelDefinitions.VAR_MODIFIED_DATE).setHeaderCaption(i18n.getMessage("header.modifiedDate"));
getColumn(SPUILabelDefinitions.VAR_MODIFIED_BY).setHeaderCaption(i18n.getMessage("header.modifiedBy")); 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_DESC).setHeaderCaption(i18n.getMessage("header.description"));
getColumn(SPUILabelDefinitions.VAR_TOTAL_TARGETS_COUNT_STATUS) getColumn(SPUILabelDefinitions.VAR_TOTAL_TARGETS_COUNT_STATUS)
.setHeaderCaption(i18n.getMessage("header.detail.status")); .setHeaderCaption(i18n.getMessage("header.detail.status"));
getColumn(SPUILabelDefinitions.VAR_STATUS).setHeaderCaption(i18n.getMessage("header.status")); getColumn(SPUILabelDefinitions.VAR_STATUS).setHeaderCaption(i18n.getMessage("header.status"));
getColumn(VIRT_PROP_RUN).setHeaderCaption(i18n.getMessage("header.action.run")); 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_PAUSE).setHeaderCaption(i18n.getMessage("header.action.pause"));
getColumn(VIRT_PROP_UPDATE).setHeaderCaption(i18n.getMessage("header.action.update")); getColumn(VIRT_PROP_UPDATE).setHeaderCaption(i18n.getMessage("header.action.update"));
getColumn(VIRT_PROP_COPY).setHeaderCaption(i18n.getMessage("header.action.copy")); getColumn(VIRT_PROP_COPY).setHeaderCaption(i18n.getMessage("header.action.copy"));
@@ -331,7 +357,7 @@ public class RolloutListGrid extends AbstractGrid<LazyQueryContainer> {
private HeaderCell joinColumns() { 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); VIRT_PROP_DELETE);
} }
@@ -346,10 +372,11 @@ public class RolloutListGrid extends AbstractGrid<LazyQueryContainer> {
final List<String> columnsToShowInOrder = Arrays.asList(ROLLOUT_RENDERER_DATA, final List<String> columnsToShowInOrder = Arrays.asList(ROLLOUT_RENDERER_DATA,
SPUILabelDefinitions.VAR_DIST_NAME_VERSION, SPUILabelDefinitions.VAR_STATUS, SPUILabelDefinitions.VAR_DIST_NAME_VERSION, SPUILabelDefinitions.VAR_STATUS,
SPUILabelDefinitions.VAR_TOTAL_TARGETS_COUNT_STATUS, SPUILabelDefinitions.VAR_NUMBER_OF_GROUPS, 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, VIRT_PROP_COPY, VIRT_PROP_DELETE, SPUILabelDefinitions.VAR_CREATED_DATE,
SPUILabelDefinitions.VAR_CREATED_USER, SPUILabelDefinitions.VAR_MODIFIED_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()); setColumns(columnsToShowInOrder.toArray());
} }
@@ -361,6 +388,7 @@ public class RolloutListGrid extends AbstractGrid<LazyQueryContainer> {
} }
getColumn(VIRT_PROP_RUN).setHidable(false); getColumn(VIRT_PROP_RUN).setHidable(false);
getColumn(VIRT_PROP_APPROVE).setHidable(false);
getColumn(VIRT_PROP_PAUSE).setHidable(false); getColumn(VIRT_PROP_PAUSE).setHidable(false);
getColumn(VIRT_PROP_DELETE).setHidable(false); getColumn(VIRT_PROP_DELETE).setHidable(false);
getColumn(VIRT_PROP_UPDATE).setHidable(false); getColumn(VIRT_PROP_UPDATE).setHidable(false);
@@ -388,6 +416,9 @@ public class RolloutListGrid extends AbstractGrid<LazyQueryContainer> {
getColumn(VIRT_PROP_RUN).setRenderer( getColumn(VIRT_PROP_RUN).setRenderer(
new GridButtonRenderer(clickEvent -> startOrResumeRollout((Long) clickEvent.getItemId())), new GridButtonRenderer(clickEvent -> startOrResumeRollout((Long) clickEvent.getItemId())),
new RolloutGridButtonConverter(this::createRunButtonMetadata)); 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( getColumn(VIRT_PROP_PAUSE).setRenderer(
new GridButtonRenderer(clickEvent -> pauseRollout((Long) clickEvent.getItemId())), new GridButtonRenderer(clickEvent -> pauseRollout((Long) clickEvent.getItemId())),
new RolloutGridButtonConverter(this::createPauseButtonMetadata)); new RolloutGridButtonConverter(this::createPauseButtonMetadata));
@@ -440,6 +471,7 @@ public class RolloutListGrid extends AbstractGrid<LazyQueryContainer> {
final GeneratedPropertyContainer decoratedContainer = getDecoratedContainer(); final GeneratedPropertyContainer decoratedContainer = getDecoratedContainer();
decoratedContainer.addGeneratedProperty(VIRT_PROP_RUN, new GenericPropertyValueGenerator()); 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_PAUSE, new GenericPropertyValueGenerator());
decoratedContainer.addGeneratedProperty(VIRT_PROP_UPDATE, new GenericPropertyValueGenerator()); decoratedContainer.addGeneratedProperty(VIRT_PROP_UPDATE, new GenericPropertyValueGenerator());
decoratedContainer.addGeneratedProperty(VIRT_PROP_COPY, new GenericPropertyValueGenerator()); decoratedContainer.addGeneratedProperty(VIRT_PROP_COPY, new GenericPropertyValueGenerator());
@@ -520,6 +552,13 @@ public class RolloutListGrid extends AbstractGrid<LazyQueryContainer> {
} }
} }
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) { private void updateRollout(final Long rolloutId) {
final CommonDialogWindow addTargetWindow = addUpdateRolloutWindow.getWindow(rolloutId, false); final CommonDialogWindow addTargetWindow = addUpdateRolloutWindow.getWindow(rolloutId, false);
addTargetWindow.setCaption(i18n.getMessage("caption.update.rollout")); addTargetWindow.setCaption(i18n.getMessage("caption.update.rollout"));
@@ -578,7 +617,7 @@ public class RolloutListGrid extends AbstractGrid<LazyQueryContainer> {
String description = null; String description = null;
if (SPUILabelDefinitions.VAR_STATUS.equals(cell.getPropertyId())) { 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())) { } else if (SPUILabelDefinitions.ACTION.equals(cell.getPropertyId())) {
description = SPUILabelDefinitions.ACTION.toLowerCase(); description = SPUILabelDefinitions.ACTION.toLowerCase();
} else if (ROLLOUT_RENDERER_DATA.equals(cell.getPropertyId())) { } else if (ROLLOUT_RENDERER_DATA.equals(cell.getPropertyId())) {
@@ -595,6 +634,12 @@ public class RolloutListGrid extends AbstractGrid<LazyQueryContainer> {
return !expectedRolloutStatus.contains(currentRolloutStatus); 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) { private StatusFontIcon createRunButtonMetadata(final RolloutStatus rolloutStatus) {
final boolean isDisabled = hasToBeDisabled(rolloutStatus, RUN_BUTTON_ENABLED); final boolean isDisabled = hasToBeDisabled(rolloutStatus, RUN_BUTTON_ENABLED);
return new StatusFontIcon(FontAwesome.PLAY, null, i18n.getMessage("tooltip.rollout.run"), return new StatusFontIcon(FontAwesome.PLAY, null, i18n.getMessage("tooltip.rollout.run"),
@@ -744,6 +789,11 @@ public class RolloutListGrid extends AbstractGrid<LazyQueryContainer> {
if (!permissionChecker.hasRolloutCreatePermission()) { if (!permissionChecker.hasRolloutCreatePermission()) {
modifiableColumnsList.remove(VIRT_PROP_COPY); 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()) { if (!permissionChecker.hasRolloutDeletePermission()) {
modifiableColumnsList.remove(VIRT_PROP_DELETE); modifiableColumnsList.remove(VIRT_PROP_DELETE);
} }

View File

@@ -14,6 +14,7 @@ import org.eclipse.hawkbit.repository.RolloutGroupManagement;
import org.eclipse.hawkbit.repository.RolloutManagement; import org.eclipse.hawkbit.repository.RolloutManagement;
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.TargetManagement;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.ui.SpPermissionChecker; import org.eclipse.hawkbit.ui.SpPermissionChecker;
import org.eclipse.hawkbit.ui.UiProperties; import org.eclipse.hawkbit.ui.UiProperties;
import org.eclipse.hawkbit.ui.common.grid.AbstractGrid; 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 EntityFactory entityFactory;
private final transient TargetFilterQueryManagement targetFilterQueryManagement; private final transient TargetFilterQueryManagement targetFilterQueryManagement;
private final transient QuotaManagement quotaManagement; private final transient QuotaManagement quotaManagement;
private final transient TenantConfigurationManagement tenantConfigManagement;
private final SpPermissionChecker permissionChecker; private final SpPermissionChecker permissionChecker;
private final RolloutUIState rolloutUIState; private final RolloutUIState rolloutUIState;
@@ -45,11 +47,13 @@ public class RolloutListView extends AbstractGridComponentLayout {
private final UiProperties uiProperties; private final UiProperties uiProperties;
public RolloutListView(final SpPermissionChecker permissionChecker, final RolloutUIState rolloutUIState, public RolloutListView(final SpPermissionChecker permissionChecker, final RolloutUIState rolloutUIState,
final UIEventBus eventBus, final RolloutManagement rolloutManagement, final UIEventBus eventBus, final RolloutManagement rolloutManagement,
final TargetManagement targetManagement, final UINotification uiNotification, final TargetManagement targetManagement, final UINotification uiNotification,
final UiProperties uiProperties, final EntityFactory entityFactory, final VaadinMessageSource i18n, final UiProperties uiProperties, final EntityFactory entityFactory,
final TargetFilterQueryManagement targetFilterQueryManagement, final VaadinMessageSource i18n,
final RolloutGroupManagement rolloutGroupManagement, final QuotaManagement quotaManagement) { final TargetFilterQueryManagement targetFilterQueryManagement,
final RolloutGroupManagement rolloutGroupManagement, final QuotaManagement quotaManagement,
final TenantConfigurationManagement tenantConfigManagement) {
super(i18n, eventBus); super(i18n, eventBus);
this.permissionChecker = permissionChecker; this.permissionChecker = permissionChecker;
this.rolloutUIState = rolloutUIState; this.rolloutUIState = rolloutUIState;
@@ -61,6 +65,7 @@ public class RolloutListView extends AbstractGridComponentLayout {
this.uiProperties = uiProperties; this.uiProperties = uiProperties;
this.entityFactory = entityFactory; this.entityFactory = entityFactory;
this.targetFilterQueryManagement = targetFilterQueryManagement; this.targetFilterQueryManagement = targetFilterQueryManagement;
this.tenantConfigManagement = tenantConfigManagement;
init(); init();
} }
@@ -76,7 +81,7 @@ public class RolloutListView extends AbstractGridComponentLayout {
public AbstractGrid<LazyQueryContainer> createGrid() { public AbstractGrid<LazyQueryContainer> createGrid() {
return new RolloutListGrid(i18n, eventBus, rolloutManagement, uiNotification, rolloutUIState, permissionChecker, return new RolloutListGrid(i18n, eventBus, rolloutManagement, uiNotification, rolloutUIState, permissionChecker,
targetManagement, entityFactory, uiProperties, targetFilterQueryManagement, rolloutGroupManagement, targetManagement, entityFactory, uiProperties, targetFilterQueryManagement, rolloutGroupManagement,
quotaManagement); quotaManagement, tenantConfigManagement);
} }
} }

View File

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

View File

@@ -62,6 +62,8 @@ public class TenantConfigurationDashboardView extends CustomComponent implements
private final PollingConfigurationView pollingConfigurationView; private final PollingConfigurationView pollingConfigurationView;
private final RolloutConfigurationView rolloutConfigurationView;
private final VaadinMessageSource i18n; private final VaadinMessageSource i18n;
private final UiProperties uiProperties; private final UiProperties uiProperties;
@@ -90,6 +92,7 @@ public class TenantConfigurationDashboardView extends CustomComponent implements
this.pollingConfigurationView = new PollingConfigurationView(i18n, controllerPollProperties, this.pollingConfigurationView = new PollingConfigurationView(i18n, controllerPollProperties,
tenantConfigurationManagement); tenantConfigurationManagement);
this.repositoryConfigurationView = new RepositoryConfigurationView(i18n, tenantConfigurationManagement); this.repositoryConfigurationView = new RepositoryConfigurationView(i18n, tenantConfigurationManagement);
this.rolloutConfigurationView = new RolloutConfigurationView(i18n, tenantConfigurationManagement, uiProperties);
this.i18n = i18n; this.i18n = i18n;
this.uiProperties = uiProperties; this.uiProperties = uiProperties;
@@ -105,7 +108,7 @@ public class TenantConfigurationDashboardView extends CustomComponent implements
configurationViews.add(defaultDistributionSetTypeLayout); configurationViews.add(defaultDistributionSetTypeLayout);
} }
configurationViews.add(repositoryConfigurationView); configurationViews.add(repositoryConfigurationView);
configurationViews.add(rolloutConfigurationView);
configurationViews.add(authenticationConfigurationView); configurationViews.add(authenticationConfigurationView);
configurationViews.add(pollingConfigurationView); configurationViews.add(pollingConfigurationView);
if (customConfigurationViews != null) { if (customConfigurationViews != null) {

View File

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

View File

@@ -133,6 +133,16 @@ public final class SPUILabelDefinitions {
* Last modified date. * Last modified date.
*/ */
public static final String VAR_MODIFIED_DATE = "modifiedDate"; 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. * Poll Status.
*/ */

View File

@@ -866,6 +866,16 @@ public final class UIComponentIdProvider {
*/ */
public static final String ROLLOUT_RUN_BUTTON_ID = ROLLOUT_ACTION_ID + ".6"; 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. * Rollout pause button id.
*/ */
@@ -1082,6 +1092,17 @@ public final class UIComponentIdProvider {
*/ */
public static final String REPOSITORY_ACTIONS_AUTOCLOSE_CHECKBOX = "repositoryactionsautoclosecheckbox"; 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. * /* Private Constructor.
*/ */

View File

@@ -38,6 +38,9 @@ button.cancel = Cancel
button.upload.file = Upload File button.upload.file = Upload File
button.no.auto.assignment = none button.no.auto.assignment = none
button.auto.assignment.desc = Select auto assign distribution set 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. bulk.targets.upload = Please upload csv file.
bulkupload.ds.name = DS Name bulkupload.ds.name = DS Name
button.discard=Discard button.discard=Discard
@@ -72,6 +75,7 @@ header.swmodules=SwModules
header.migrations.step=IsRequiredMigrationStep header.migrations.step=IsRequiredMigrationStep
header.action=Actions header.action=Actions
header.action.run=Run header.action.run=Run
header.action.approve=Approve
header.action.pause=Pause header.action.pause=Pause
header.action.update=Edit header.action.update=Edit
header.action.copy=Copy 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.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.auto.assign.enable=Enable auto assignment
label.scheduled=Scheduled label.scheduled=Scheduled
label.approval.decision = Approval decision
label.approval.remark = Remark (optional)
# Checkbox label prefix with - checkbox # Checkbox label prefix with - checkbox
checkbox.dist.migration.required = Required Migration Step : checkbox.dist.migration.required = Required Migration Step :
@@ -254,6 +260,7 @@ tooltip.metadata.icon = Manage Metadata
tooltip.next.maintenancewindow = next on {0} tooltip.next.maintenancewindow = next on {0}
#rollout action #rollout action
tooltip.rollout.run = Run tooltip.rollout.run = Run
tooltip.rollout.approve = Approve
tooltip.rollout.pause = Pause tooltip.rollout.pause = Pause
tooltip.rollout.update = Update.. tooltip.rollout.update = Update..
tooltip.rollout.copy = Copy.. tooltip.rollout.copy = Copy..
@@ -483,6 +490,8 @@ configuration.polling.title=Polling Configuration
configuration.polling.time=Polling Time configuration.polling.time=Polling Time
configuration.polling.overduetime=Polling Overdue Time configuration.polling.overduetime=Polling Overdue Time
configuration.polling.custom.value=use a custom value 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
calendar.year=year calendar.year=year
@@ -511,13 +520,15 @@ header.assigned.ds = Assigned DS
header.installed.ds = Installed DS header.installed.ds = Installed DS
header.target.status = Status header.target.status = Status
header.target.tags = Tags header.target.tags = Tags
header.total.targets = Targets header.total.targets = Targets
header.key = Key header.key = Key
header.value = Value header.value = Value
metadata.targetvisible = Visible for targets metadata.targetvisible = Visible for targets
header.auto.assignment.ds = Auto assignment header.auto.assignment.ds = Auto assignment
header.target.filter.name = Target filter name header.target.filter.name = Target filter name
header.target.filter.query = Target filter query header.target.filter.query = Target filter query
header.approvalDecidedBy = Decided By
header.approvalRemark = Approval Remark
distribution.details.header = Distribution set distribution.details.header = Distribution set
target.details.header = Target target.details.header = Target
@@ -556,6 +567,7 @@ caption.configure.rollout = Configure Rollout
caption.configure.rollout.groups = Configure Deployment Groups caption.configure.rollout.groups = Configure Deployment Groups
caption.update.rollout = Update Rollout caption.update.rollout = Update Rollout
caption.create.rollout = Create new Rollout caption.create.rollout = Create new Rollout
caption.approve.rollout = Approve Rollout
prompt.target.filter = Custom Target Filter prompt.target.filter = Custom Target Filter
message.rollout.nonzero.group.number = Number of groups must be greater than zero 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 message.rollout.max.group.number = Number of groups must not be greater than 500