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 a _DistributionSet_.
- Auto-splitting of the input target list into a defined number deployment groups.
- Approval workflow
- Has to be enabled explicitly in configuration.
- Enables a workflow that requires a user with adequate permissions to review any new or updated rollout before it
can be started.
- Allows integration with 3rd party workflow engines.
- Cascading start of the deployment groups based on installation status of the previous group.
- Emergency shutdown of the rollout in case a group exceeds the defined error threshold.
- Rollout progress monitoring for the entire rollout and the individual groups.

View File

@@ -340,6 +340,54 @@ public interface RolloutManagement {
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_HANDLE)
void resumeRollout(long rolloutId);
/**
* Approves or denies a created rollout being in state
* {@link RolloutStatus#WAITING_FOR_APPROVAL}. If the rollout is approved, it
* switches state to {@link RolloutStatus#READY}, otherwise it switches to state
* {@link RolloutStatus#APPROVAL_DENIED}
*
* @param rolloutId
* the rollout to be approved or denied.
* @param decision
* decision whether a rollout is approved or denied.
*
* @return approved or denied rollout
*
* @throws EntityNotFoundException
* if rollout with given ID does not exist
* @throws RolloutIllegalStateException
* if given rollout is not in
* {@link RolloutStatus#WAITING_FOR_APPROVAL}. Only rollouts
* waiting for approval can be acted upon.
*/
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_APPROVE)
Rollout approveOrDeny(long rolloutId, Rollout.ApprovalDecision decision);
/**
* Approves or denies a created rollout being in state
* {@link RolloutStatus#WAITING_FOR_APPROVAL}. If the rollout is approved, it
* switches state to {@link RolloutStatus#READY}, otherwise it switches to state
* {@link RolloutStatus#APPROVAL_DENIED}
*
* @param rolloutId
* the rollout to be approved or denied.
* @param decision
* decision whether a rollout is approved or denied.
* @param remark
* user remark on approve / deny decision
*
* @return approved or denied rollout
*
* @throws EntityNotFoundException
* if rollout with given ID does not exist
* @throws RolloutIllegalStateException
* if given rollout is not in
* {@link RolloutStatus#WAITING_FOR_APPROVAL}. Only rollouts
* waiting for approveOrDeny can be acted upon.
*/
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_APPROVE)
Rollout approveOrDeny(long rolloutId, Rollout.ApprovalDecision decision, String remark);
/**
* Starts a rollout which has been created. The rollout must be in
* {@link RolloutStatus#READY} state. The Rollout will be set into the

View File

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

View File

@@ -114,6 +114,11 @@ public class TenantConfigurationProperties {
*/
public static final String ANONYMOUS_DOWNLOAD_MODE_ENABLED = "anonymous.download.enabled";
/**
* Represents setting if approval for a rollout is needed.
*/
public static final String ROLLOUT_APPROVAL_ENABLED = "rollout.approval.enabled";
/**
* Repository on autoclose mode instead of canceling in case of new DS
* assignment over active actions.

View File

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

View File

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

View File

@@ -71,4 +71,10 @@ hawkbit.server.tenant.configuration.anonymous-download-enabled.keyName=anonymous
hawkbit.server.tenant.configuration.anonymous-download-enabled.defaultValue=${hawkbit.server.download.anonymous.enabled}
hawkbit.server.tenant.configuration.anonymous-download-enabled.dataType=java.lang.Boolean
hawkbit.server.tenant.configuration.anonymous-download-enabled.validator=org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationBooleanValidator
hawkbit.server.tenant.configuration.rollout-approval-enabled.keyName=rollout.approval.enabled
hawkbit.server.tenant.configuration.rollout-approval-enabled.defaultValue=false
hawkbit.server.tenant.configuration.rollout-approval-enabled.dataType=java.lang.Boolean
hawkbit.server.tenant.configuration.rollout-approval-enabled.validator=org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationBooleanValidator
# Default tenant configuration - END

View File

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

View File

@@ -25,6 +25,7 @@ import org.eclipse.hawkbit.repository.AbstractRolloutManagement;
import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.DistributionSetManagement;
import org.eclipse.hawkbit.repository.QuotaManagement;
import org.eclipse.hawkbit.repository.RolloutApprovalStrategy;
import org.eclipse.hawkbit.repository.RolloutFields;
import org.eclipse.hawkbit.repository.RolloutGroupManagement;
import org.eclipse.hawkbit.repository.RolloutHelper;
@@ -157,9 +158,9 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
final DistributionSetManagement distributionSetManagement, final ApplicationContext context,
final ApplicationEventPublisher eventPublisher, final VirtualPropertyReplacer virtualPropertyReplacer,
final PlatformTransactionManager txManager, final TenantAware tenantAware, final LockRegistry lockRegistry,
final Database database) {
final Database database, final RolloutApprovalStrategy rolloutApprovalStrategy) {
super(targetManagement, deploymentManagement, rolloutGroupManagement, distributionSetManagement, context,
eventPublisher, virtualPropertyReplacer, txManager, tenantAware, lockRegistry);
eventPublisher, virtualPropertyReplacer, txManager, tenantAware, lockRegistry, rolloutApprovalStrategy);
this.database = database;
}
@@ -357,8 +358,14 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
// When all groups are ready the rollout status can be changed to be
// ready, too.
if (readyGroups == rolloutGroups.size()) {
LOGGER.debug("rollout {} creation done. Switch to READY.", rollout.getId());
rollout.setStatus(RolloutStatus.READY);
if (!rolloutApprovalStrategy.isApprovalNeeded(rollout)) {
rollout.setStatus(RolloutStatus.READY);
LOGGER.debug("rollout {} creation done. Switch to READY.", rollout.getId());
} else {
LOGGER.debug("rollout {} creation done. Switch to WAITING_FOR_APPROVAL.", rollout.getId());
rollout.setStatus(RolloutStatus.WAITING_FOR_APPROVAL);
rolloutApprovalStrategy.onApprovalRequired(rollout);
}
rollout.setLastCheck(0);
rollout.setTotalTargets(totalTargets);
rolloutRepository.save(rollout);
@@ -451,6 +458,39 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
groups.stream().map(RolloutGroupCreate::build).collect(Collectors.toList()), baseFilter, totalTargets));
}
@Override
@Transactional
@Retryable(include = {
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public Rollout approveOrDeny(final long rolloutId, final Rollout.ApprovalDecision decision) {
return this.approveOrDeny(rolloutId, decision, null);
}
@Override
@Transactional
@Retryable(include = {
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public Rollout approveOrDeny(final long rolloutId, final Rollout.ApprovalDecision decision, final String remark) {
LOGGER.debug("approveOrDeny rollout called for rollout {} with decision {}", rolloutId, decision);
final JpaRollout rollout = getRolloutAndThrowExceptionIfNotFound(rolloutId);
RolloutHelper.verifyRolloutInStatus(rollout, RolloutStatus.WAITING_FOR_APPROVAL);
switch (decision) {
case APPROVED:
rollout.setStatus(RolloutStatus.READY);
break;
case DENIED:
rollout.setStatus(RolloutStatus.APPROVAL_DENIED);
break;
default:
throw new IllegalArgumentException("Unknown approval decision: " + decision);
}
rollout.setApprovalDecidedBy(rolloutApprovalStrategy.getApprovalUser(rollout));
if (remark != null) {
rollout.setApprovalRemark(remark);
}
return rolloutRepository.save(rollout);
}
@Override
@Transactional
@Retryable(include = {
@@ -964,7 +1004,6 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
final JpaRollout rollout = getRolloutAndThrowExceptionIfNotFound(update.getId());
checkIfDeleted(update.getId(), rollout.getStatus());
update.getName().ifPresent(rollout::setName);
update.getDescription().ifPresent(rollout::setDescription);
update.getActionType().ifPresent(rollout::setActionType);
@@ -976,6 +1015,11 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
rollout.setDistributionSet(set);
});
if (rolloutApprovalStrategy.isApprovalNeeded(rollout)) {
rollout.setStatus(RolloutStatus.WAITING_FOR_APPROVAL);
rollout.setApprovalDecidedBy(null);
rollout.setApprovalRemark(null);
}
return rolloutRepository.save(rollout);
}
@@ -1067,7 +1111,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
/**
* Enforces the quota defining the maximum number of {@link Target}s per
* {@link RolloutGroup}.
*
*
* @param group
* The rollout group
* @param requested
@@ -1081,7 +1125,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
/**
* Enforces the quota defining the maximum number of {@link Action}s per
* {@link Target}.
*
*
* @param target
* The target
* @param requested

View File

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

View File

@@ -88,7 +88,9 @@ public class JpaRollout extends AbstractJpaNamedEntity implements Rollout, Event
@ConversionValue(objectValue = "ERROR_CREATING", dataValue = "7"),
@ConversionValue(objectValue = "ERROR_STARTING", dataValue = "8"),
@ConversionValue(objectValue = "DELETING", dataValue = "9"),
@ConversionValue(objectValue = "DELETED", dataValue = "10") })
@ConversionValue(objectValue = "DELETED", dataValue = "10"),
@ConversionValue(objectValue = "WAITING_FOR_APPROVAL", dataValue = "11"),
@ConversionValue(objectValue = "APPROVAL_DENIED", dataValue = "12")})
@Convert("rolloutstatus")
@NotNull
private RolloutStatus status = RolloutStatus.CREATING;
@@ -120,6 +122,14 @@ public class JpaRollout extends AbstractJpaNamedEntity implements Rollout, Event
@Column(name = "start_at")
private Long startAt;
@Column(name = "approval_decided_by")
@Size(min = 1, max = Rollout.APPROVED_BY_MAX_SIZE)
private String approvalDecidedBy;
@Column(name = "approval_remark")
@Size(max = Rollout.APPROVAL_REMARK_MAX_SIZE)
private String approvalRemark;
@Transient
private transient TotalTargetCountStatus totalTargetCountStatus;
@@ -271,4 +281,22 @@ public class JpaRollout extends AbstractJpaNamedEntity implements Rollout, Event
this.deleted = deleted;
}
@Override
public String getApprovalDecidedBy() {
return approvalDecidedBy;
}
public void setApprovalDecidedBy(final String approvalDecidedBy) {
this.approvalDecidedBy = approvalDecidedBy;
}
@Override
public String getApprovalRemark() {
return approvalRemark;
}
public void setApprovalRemark(final String approvalRemark) {
this.approvalRemark = approvalRemark;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -68,12 +68,17 @@ import org.eclipse.hawkbit.repository.model.TotalTargetCountStatus;
import org.eclipse.hawkbit.repository.test.matcher.Expect;
import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import ru.yandex.qatools.allure.annotations.Description;
import ru.yandex.qatools.allure.annotations.Features;
import ru.yandex.qatools.allure.annotations.Step;
@@ -87,6 +92,12 @@ import ru.yandex.qatools.allure.annotations.Title;
@Stories("Rollout Management")
public class RolloutManagementTest extends AbstractJpaIntegrationTest {
@Before
@After
public void reset() {
this.approvalStrategy.setApprovalNeeded(false);
}
@Test
@Description("Verifies that a running action with distribution-set (A) is not canceled by a rollout which tries to also assign a distribution-set (A)")
public void rolloutShouldNotCancelRunningActionWithTheSameDistributionSet() {
@@ -1539,6 +1550,59 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest {
}
@Test
@Description("Creating a rollout with approval role or approval engine disabled results in the rollout being in " +
"READY state.")
public void createdRolloutWithApprovalRoleOrApprovalDisabledTransitionsToReadyState() {
approvalStrategy.setApprovalNeeded(false);
final String successCondition = "50";
final String errorCondition = "80";
final Rollout rollout = createSimpleTestRolloutWithTargetsAndDistributionSet(10, 10,
5, successCondition, errorCondition);
assertThat(rollout.getStatus()).isEqualTo(Rollout.RolloutStatus.READY);
}
@Test
@Description("Creating a rollout without approver role and approval enabled leads to transition to " +
"WAITING_FOR_APPROVAL state.")
public void createdRolloutWithoutApprovalRoleTransitionsToWaitingForApprovalState() {
approvalStrategy.setApprovalNeeded(true);
final String successCondition = "50";
final String errorCondition = "80";
final Rollout rollout = createSimpleTestRolloutWithTargetsAndDistributionSet(10, 10,
5, successCondition, errorCondition);
assertThat(rollout.getStatus()).isEqualTo(Rollout.RolloutStatus.WAITING_FOR_APPROVAL);
}
@Test
@Description("Approving a rollout leads to transition to READY state.")
public void approvedRolloutTransitionsToReadyState() {
approvalStrategy.setApprovalNeeded(true);
final String successCondition = "50";
final String errorCondition = "80";
final Rollout rollout = createSimpleTestRolloutWithTargetsAndDistributionSet(10, 10,
5, successCondition, errorCondition);
assertThat(rollout.getStatus()).isEqualTo(Rollout.RolloutStatus.WAITING_FOR_APPROVAL);
rolloutManagement.approveOrDeny(rollout.getId(), Rollout.ApprovalDecision.APPROVED);
final Rollout resultingRollout = rolloutRepository.findOne(rollout.getId());
assertThat(resultingRollout.getStatus()).isEqualTo(Rollout.RolloutStatus.READY);
}
@Test
@Description("Denying approval for a rollout leads to transition to APPROVAL_DENIED state.")
public void deniedRolloutTransitionsToApprovalDeniedState() {
approvalStrategy.setApprovalNeeded(true);
final String successCondition = "50";
final String errorCondition = "80";
final Rollout rollout = createSimpleTestRolloutWithTargetsAndDistributionSet(10, 10,
5, successCondition, errorCondition);
assertThat(rollout.getStatus()).isEqualTo(Rollout.RolloutStatus.WAITING_FOR_APPROVAL);
rolloutManagement.approveOrDeny(rollout.getId(), Rollout.ApprovalDecision.DENIED);
final Rollout resultingRollout = rolloutRepository.findOne(rollout.getId());
assertThat(resultingRollout.getStatus()).isEqualTo(RolloutStatus.APPROVAL_DENIED);
}
@Test
@ExpectEvents({ @Expect(type = RolloutDeletedEvent.class, count = 1),
@Expect(type = DistributionSetCreatedEvent.class, count = 1),

View File

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

View File

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

View File

@@ -81,6 +81,36 @@ public interface MgmtRolloutRestApi {
MediaType.APPLICATION_JSON_VALUE })
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.
*

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).pause(rollout.getId())).withRel("pause"));
body.add(linkTo(methodOn(MgmtRolloutRestApi.class).resume(rollout.getId())).withRel("resume"));
body.add(linkTo(methodOn(MgmtRolloutRestApi.class).approve(rollout.getId(), null)).withRel("approve"));
body.add(linkTo(methodOn(MgmtRolloutRestApi.class).deny(rollout.getId(), null)).withRel("deny"));
body.add(linkTo(methodOn(MgmtRolloutRestApi.class).getRolloutGroups(rollout.getId(),
MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET_VALUE,
MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT_VALUE, null, null)).withRel("groups"));

View File

@@ -129,6 +129,18 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi {
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
public ResponseEntity<Void> start(@PathVariable("rolloutId") final Long rolloutId) {
this.rolloutManagement.start(rolloutId);

View File

@@ -179,6 +179,81 @@ include::../errors/415.adoc[]
include::../errors/429.adoc[]
|===
== POST /rest/v1/rollouts/{rolloutId}/approve
=== Implementation Notes
Handles the POST request of approving a created rollout within SP.
Only possible if approval workflow is enabled in system configuration and rollout is in state WAITING_FOR_APPROVAL.
Required Permission: APPROVE_ROLLOUT
=== Approve Rollout
==== CURL
include::{snippets}/rollouts/approve-rollout/curl-request.adoc[]
==== Request URL
include::{snippets}/rollouts/approve-rollout/http-request.adoc[]
=== Response (Status 200)
==== Response example
include::{snippets}/rollouts/approve-rollout/http-response.adoc[]
=== Error responses
|===
| HTTP Status Code | Reason | Response Model
include::../errors/400.adoc[]
include::../errors/401.adoc[]
include::../errors/403.adoc[]
include::../errors/405.adoc[]
include::../errors/406.adoc[]
include::../errors/429.adoc[]
|===
== POST /rest/v1/rollouts/{rolloutId}/deny
=== Implementation Notes
Handles the POST request of denying a created rollout within SP.
Only possible if approval workflow is enabled in system configuration and rollout is in state WAITING_FOR_APPROVAL.
Required Permission: APPROVE_ROLLOUT
=== Deny Rollout
==== CURL
include::{snippets}/rollouts/deny-rollout/curl-request.adoc[]
==== Request URL
include::{snippets}/rollouts/deny-rollout/http-request.adoc[]
=== Response (Status 200)
==== Response example
include::{snippets}/rollouts/deny-rollout/http-response.adoc[]
=== Error responses
|===
| HTTP Status Code | Reason | Response Model
include::../errors/400.adoc[]
include::../errors/401.adoc[]
include::../errors/403.adoc[]
include::../errors/405.adoc[]
include::../errors/406.adoc[]
include::../errors/429.adoc[]
|===
== POST /rest/v1/rollouts/{rolloutId}/start
=== Implementation Notes

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_PAUSE = "Link to pause a running rollout";
public static final String ROLLOUT_LINKS_RESUME = "Link to resume a paused rollout";
public static final String ROLLOUT_LINKS_APPROVE = "Link to approve a rollout";
public static final String ROLLOUT_LINKS_DENY = "Link to deny a rollout";
public static final String ROLLOUT_LINKS_GROUPS = "Link to retrieve the groups a rollout";
public static final String ROLLOUT_START_ASYNC = "Start the rollout asynchronous";

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.RolloutGroupConditionBuilder;
import org.eclipse.hawkbit.repository.model.RolloutGroupConditions;
import org.eclipse.hawkbit.repository.test.util.RolloutTestApprovalStrategy;
import org.eclipse.hawkbit.rest.documentation.AbstractApiRestDocumentation;
import org.eclipse.hawkbit.rest.documentation.ApiModelPropertiesGeneric;
import org.eclipse.hawkbit.rest.documentation.DocumenationResponseFieldsSnippet;
@@ -41,6 +42,7 @@ import org.eclipse.hawkbit.rest.util.JsonBuilder;
import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.MediaType;
@@ -62,12 +64,16 @@ import ru.yandex.qatools.allure.annotations.Stories;
@Stories("Rollout Resource")
public class RolloutResourceDocumentationTest extends AbstractApiRestDocumentation {
@Autowired
private RolloutTestApprovalStrategy approvalStrategy;
@Override
@Before
public void setUp() {
this.resourceName = "rollouts";
super.setUp();
arrayPrefix = "content[].";
approvalStrategy.setApprovalNeeded(false);
}
@Test
@@ -127,7 +133,6 @@ public class RolloutResourceDocumentationTest extends AbstractApiRestDocumentati
allFieldDescriptor.add(
fieldWithPath(arrayPrefix + "totalTargets").description(MgmtApiModelProperties.ROLLOUT_TOTAL_TARGETS));
allFieldDescriptor.add(fieldWithPath(arrayPrefix + "_links.self").ignored());
if (withDetails) {
allFieldDescriptor.add(fieldWithPath(arrayPrefix + "totalTargetsPerStatus")
.description(MgmtApiModelProperties.ROLLOUT_TOTAL_TARGETS_PER_STATUS));
@@ -139,6 +144,10 @@ public class RolloutResourceDocumentationTest extends AbstractApiRestDocumentati
.description(MgmtApiModelProperties.ROLLOUT_LINKS_RESUME));
allFieldDescriptor.add(fieldWithPath(arrayPrefix + "_links.groups")
.description(MgmtApiModelProperties.ROLLOUT_LINKS_GROUPS));
allFieldDescriptor.add(fieldWithPath(arrayPrefix + "_links.approve")
.description(MgmtApiModelProperties.ROLLOUT_LINKS_APPROVE));
allFieldDescriptor.add(fieldWithPath(arrayPrefix + "_links.deny")
.description(MgmtApiModelProperties.ROLLOUT_LINKS_DENY));
}
return new DocumenationResponseFieldsSnippet(allFieldDescriptor);
@@ -386,6 +395,29 @@ public class RolloutResourceDocumentationTest extends AbstractApiRestDocumentati
pathParameters(parameterWithName("rolloutId").description(ApiModelPropertiesGeneric.ITEM_ID))));
}
@Test
@Description("Handles the POST request of approving a rollout. Required Permission: "
+ SpPermission.APPROVE_ROLLOUT)
public void approveRollout() throws Exception {
approvalStrategy.setApprovalNeeded(true);
final Rollout rollout = createRolloutEntity();
mockMvc.perform(post(MgmtRestConstants.ROLLOUT_V1_REQUEST_MAPPING + "/{rolloutId}/approve", rollout.getId())
.accept(MediaTypes.HAL_JSON_VALUE)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
.andDo(this.document.document(
pathParameters(parameterWithName("rolloutId").description(ApiModelPropertiesGeneric.ITEM_ID))));
}
@Test
@Description("Handles the POST request of denying a rollout. Required Permission: " + SpPermission.APPROVE_ROLLOUT)
public void denyRollout() throws Exception {
approvalStrategy.setApprovalNeeded(true);
final Rollout rollout = createRolloutEntity();
mockMvc.perform(post(MgmtRestConstants.ROLLOUT_V1_REQUEST_MAPPING + "/{rolloutId}/deny", rollout.getId())
.accept(MediaTypes.HAL_JSON_VALUE)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
.andDo(this.document.document(
pathParameters(parameterWithName("rolloutId").description(ApiModelPropertiesGeneric.ITEM_ID))));
}
@Test
@Description("Handles the GET request of retrieving the deploy groups of a rollout. Required Permission: "
+ SpPermission.READ_ROLLOUT)

View File

@@ -80,6 +80,8 @@ public class TenantResourceDocumentationTest extends AbstractApiRestDocumentatio
"if the anonymous download mode is enabled.");
CONFIG_ITEM_DESCRIPTIONS.put(TenantConfigurationKey.REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED,
"if autoclose running actions with new Distribution Set assignment is enabled.");
CONFIG_ITEM_DESCRIPTIONS.put(TenantConfigurationKey.ROLLOUT_APPROVAL_ENABLED,
"if approval mode for Rollout Management is enabled.");
}
@Autowired

View File

@@ -42,6 +42,7 @@ spring.http.multipart.max-request-size=-1
# UI help links
hawkbit.server.ui.links.documentation.root=https://www.eclipse.org/hawkbit/documentation/overview/introduction.html
hawkbit.server.ui.links.documentation.security=https://www.eclipse.org/hawkbit/documentation/security/security.html
hawkbit.server.ui.links.documentation.rollout=https://www.eclipse.org/hawkbit/documentation/architecture/rollout-management.html
hawkbit.server.ui.links.documentation.deployment-view=https://www.eclipse.org/hawkbit/documentation/interfaces/management-ui.html
hawkbit.server.ui.links.documentation.distribution-view=https://www.eclipse.org/hawkbit/documentation/interfaces/management-ui.html

View File

@@ -145,6 +145,11 @@ public final class SpPermission {
*/
public static final String HANDLE_ROLLOUT = "HANDLE_ROLLOUT";
/**
* Permission to approve or deny a rollout prior to starting.
*/
public static final String APPROVE_ROLLOUT = "APPROVE_ROLLOUT";
private SpPermission() {
// Constants only
}
@@ -389,6 +394,14 @@ public final class SpPermission {
public static final String HAS_AUTH_ROLLOUT_MANAGEMENT_HANDLE = HAS_AUTH_PREFIX + HANDLE_ROLLOUT
+ HAS_AUTH_SUFFIX + HAS_AUTH_OR + IS_SYSTEM_CODE;
/**
* Spring security eval hasAuthority expression to check if spring
* context contains {@link SpPermission#APPROVE_ROLLOUT} or
* {@link #IS_SYSTEM_CODE}.
*/
public static final String HAS_AUTH_ROLLOUT_MANAGEMENT_APPROVE = HAS_AUTH_PREFIX + APPROVE_ROLLOUT
+ HAS_AUTH_SUFFIX + HAS_AUTH_OR + IS_SYSTEM_CODE;
/**
* Spring security eval hasAuthority expression to check if spring
* context contains {@link SpPermission#UPDATE_ROLLOUT} or

View File

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

View File

@@ -151,4 +151,12 @@ public class SpPermissionChecker implements Serializable {
public boolean hasRolloutTargetsReadPermission() {
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 = "";
/**
* Link to rollout related documentation.
*/
private String rollout = "";
/**
* Link to target filter view.
*/
@@ -167,6 +172,10 @@ public class UiProperties implements Serializable {
return security;
}
public String getRollout() {
return rollout;
}
public String getSystemConfigurationView() {
return systemConfigurationView;
}
@@ -203,6 +212,10 @@ public class UiProperties implements Serializable {
this.security = security;
}
public void setRollout(final String rollout) {
this.rollout = rollout;
}
public void setSystemConfigurationView(final String systemConfigurationView) {
this.systemConfigurationView = systemConfigurationView;
}

View File

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

View File

@@ -78,9 +78,11 @@ import com.vaadin.data.util.converter.StringToIntegerConverter;
import com.vaadin.data.validator.IntegerRangeValidator;
import com.vaadin.data.validator.LongRangeValidator;
import com.vaadin.data.validator.NullValidator;
import com.vaadin.server.FontAwesome;
import com.vaadin.shared.ui.label.ContentMode;
import com.vaadin.ui.ComboBox;
import com.vaadin.ui.GridLayout;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Label;
import com.vaadin.ui.OptionGroup;
import com.vaadin.ui.TabSheet;
@@ -106,6 +108,10 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
private static final String MESSAGE_ENTER_NUMBER = "message.enter.number";
private static final String APPROVAL_BUTTON_LABEL = "button.approve";
private static final String DENY_BUTTON_LABEL = "button.deny";
private final ActionTypeOptionGroupLayout actionTypeOptionGroupLayout;
private final AutoStartOptionGroupLayout autoStartOptionGroupLayout;
@@ -150,6 +156,10 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
private OptionGroup errorThresholdOptionGroup;
private Label approvalLabel;
private HorizontalLayout approvalButtonsLayout;
private CommonDialogWindow window;
private boolean editRolloutEnabled;
@@ -166,6 +176,10 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
private GroupsLegendLayout groupsLegendLayout;
private OptionGroup approveButtonsGroup;
private TextField approvalRemarkField;
private final transient RolloutGroupConditions defaultRolloutGroupConditions;
private final NullValidator nullValidator = new NullValidator(null, false);
@@ -213,6 +227,11 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
if (editRolloutEnabled) {
editRollout();
if (rollout.getStatus().equals(Rollout.RolloutStatus.WAITING_FOR_APPROVAL)) {
rolloutManagement.approveOrDeny(rollout.getId(),
(Rollout.ApprovalDecision) approveButtonsGroup.getValue(), approvalRemarkField.getValue());
eventBus.publish(this, RolloutEvent.UPDATE_ROLLOUT);
}
return;
}
createRollout();
@@ -413,6 +432,12 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
rollout = null;
groupsDefinitionTabs.setVisible(true);
groupsDefinitionTabs.setSelectedTab(0);
approvalLabel.setVisible(false);
approvalButtonsLayout.setVisible(false);
approveButtonsGroup.clear();
approvalRemarkField.clear();
approveButtonsGroup.removeAllValidators();
}
private void addGroupsDefinitionTabs() {
@@ -443,7 +468,7 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
setSpacing(true);
setSizeUndefined();
setRows(7);
setRows(8);
setColumns(4);
setStyleName("marginTop");
setColumnExpandRatio(3, 1);
@@ -477,6 +502,9 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
addComponent(groupsDefinitionTabs, 0, 6, 3, 6);
addComponent(approvalLabel, 0, 7);
addComponent(approvalButtonsLayout, 1, 7, 3, 7);
rolloutName.focus();
}
@@ -541,6 +569,8 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
groupsLegendLayout = new GroupsLegendLayout(i18n);
approvalLabel = getLabel("label.approval.decision");
approvalButtonsLayout = createApprovalLayout();
}
private void displayValidationStatus(final DefineGroupsLayout.ValidationStatus status) {
@@ -610,6 +640,29 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
return layout;
}
private HorizontalLayout createApprovalLayout() {
approveButtonsGroup = new OptionGroup();
approveButtonsGroup.setId(UIComponentIdProvider.ROLLOUT_APPROVAL_OPTIONGROUP_ID);
approveButtonsGroup.addStyleName(ValoTheme.OPTIONGROUP_SMALL);
approveButtonsGroup.addStyleName(ValoTheme.OPTIONGROUP_HORIZONTAL);
approveButtonsGroup.addStyleName("custom-option-group");
approveButtonsGroup.addItems(Rollout.ApprovalDecision.APPROVED, Rollout.ApprovalDecision.DENIED);
approveButtonsGroup.setItemCaption(Rollout.ApprovalDecision.APPROVED, i18n.getMessage(APPROVAL_BUTTON_LABEL));
approveButtonsGroup.setItemIcon(Rollout.ApprovalDecision.APPROVED, FontAwesome.CHECK);
approveButtonsGroup.setItemCaption(Rollout.ApprovalDecision.DENIED, i18n.getMessage(DENY_BUTTON_LABEL));
approveButtonsGroup.setItemIcon(Rollout.ApprovalDecision.DENIED, FontAwesome.TIMES);
approvalRemarkField = createTextField("label.approval.remark",
UIComponentIdProvider.ROLLOUT_APPROVAL_REMARK_FIELD_ID, Rollout.APPROVAL_REMARK_MAX_SIZE);
approvalRemarkField.setWidth(100.0F, Unit.PERCENTAGE);
HorizontalLayout layout = new HorizontalLayout(approveButtonsGroup, approvalRemarkField);
layout.setWidth(100.0F, Unit.PERCENTAGE);
layout.setExpandRatio(approvalRemarkField, 1.0F);
return layout;
}
private static Label createCountLabel() {
final Label groupSize = new LabelBuilder().visible(false).name("").buildLabel();
groupSize.addStyleName(ValoTheme.LABEL_TINY + " " + "rollout-target-count-message");
@@ -966,6 +1019,13 @@ public class AddUpdateRolloutWindowLayout extends GridLayout {
if (rollout.getStatus() != Rollout.RolloutStatus.READY) {
disableRequiredFieldsOnEdit();
}
if (rollout.getStatus() == Rollout.RolloutStatus.WAITING_FOR_APPROVAL) {
approvalButtonsLayout.setVisible(true);
approveButtonsGroup.addValidator(nullValidator);
approvalLabel.setVisible(true);
}
rolloutName.setValue(rollout.getName());
groupsDefinitionTabs.setVisible(false);

View File

@@ -43,6 +43,8 @@ public class ProxyRollout {
private long forcedTime;
private RolloutStatus status;
private TotalTargetCountStatus totalTargetCountStatus;
private String approvalDecidedBy;
private String approvalRemark;
public RolloutRendererData getRolloutRendererData() {
return rolloutRendererData;
@@ -217,4 +219,20 @@ public class ProxyRollout {
public void setTotalTargetCountStatus(final TotalTargetCountStatus totalTargetCountStatus) {
this.totalTargetCountStatus = totalTargetCountStatus;
}
public String getApprovalDecidedBy() {
return approvalDecidedBy;
}
public void setApprovalDecidedBy(final String approvalDecidedBy) {
this.approvalDecidedBy = approvalDecidedBy;
}
public String getApprovalRemark() {
return approvalRemark;
}
public void setApprovalRemark(final String approvalRemark) {
this.approvalRemark = approvalRemark;
}
}

View File

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

View File

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

View File

@@ -14,6 +14,7 @@ import org.eclipse.hawkbit.repository.RolloutGroupManagement;
import org.eclipse.hawkbit.repository.RolloutManagement;
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
import org.eclipse.hawkbit.repository.TargetManagement;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.ui.SpPermissionChecker;
import org.eclipse.hawkbit.ui.UiProperties;
import org.eclipse.hawkbit.ui.common.grid.AbstractGrid;
@@ -38,6 +39,7 @@ public class RolloutListView extends AbstractGridComponentLayout {
private final transient EntityFactory entityFactory;
private final transient TargetFilterQueryManagement targetFilterQueryManagement;
private final transient QuotaManagement quotaManagement;
private final transient TenantConfigurationManagement tenantConfigManagement;
private final SpPermissionChecker permissionChecker;
private final RolloutUIState rolloutUIState;
@@ -45,11 +47,13 @@ public class RolloutListView extends AbstractGridComponentLayout {
private final UiProperties uiProperties;
public RolloutListView(final SpPermissionChecker permissionChecker, final RolloutUIState rolloutUIState,
final UIEventBus eventBus, final RolloutManagement rolloutManagement,
final TargetManagement targetManagement, final UINotification uiNotification,
final UiProperties uiProperties, final EntityFactory entityFactory, final VaadinMessageSource i18n,
final TargetFilterQueryManagement targetFilterQueryManagement,
final RolloutGroupManagement rolloutGroupManagement, final QuotaManagement quotaManagement) {
final UIEventBus eventBus, final RolloutManagement rolloutManagement,
final TargetManagement targetManagement, final UINotification uiNotification,
final UiProperties uiProperties, final EntityFactory entityFactory,
final VaadinMessageSource i18n,
final TargetFilterQueryManagement targetFilterQueryManagement,
final RolloutGroupManagement rolloutGroupManagement, final QuotaManagement quotaManagement,
final TenantConfigurationManagement tenantConfigManagement) {
super(i18n, eventBus);
this.permissionChecker = permissionChecker;
this.rolloutUIState = rolloutUIState;
@@ -61,6 +65,7 @@ public class RolloutListView extends AbstractGridComponentLayout {
this.uiProperties = uiProperties;
this.entityFactory = entityFactory;
this.targetFilterQueryManagement = targetFilterQueryManagement;
this.tenantConfigManagement = tenantConfigManagement;
init();
}
@@ -76,7 +81,7 @@ public class RolloutListView extends AbstractGridComponentLayout {
public AbstractGrid<LazyQueryContainer> createGrid() {
return new RolloutListGrid(i18n, eventBus, rolloutManagement, uiNotification, rolloutUIState, permissionChecker,
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 RolloutConfigurationView rolloutConfigurationView;
private final VaadinMessageSource i18n;
private final UiProperties uiProperties;
@@ -90,6 +92,7 @@ public class TenantConfigurationDashboardView extends CustomComponent implements
this.pollingConfigurationView = new PollingConfigurationView(i18n, controllerPollProperties,
tenantConfigurationManagement);
this.repositoryConfigurationView = new RepositoryConfigurationView(i18n, tenantConfigurationManagement);
this.rolloutConfigurationView = new RolloutConfigurationView(i18n, tenantConfigurationManagement, uiProperties);
this.i18n = i18n;
this.uiProperties = uiProperties;
@@ -105,7 +108,7 @@ public class TenantConfigurationDashboardView extends CustomComponent implements
configurationViews.add(defaultDistributionSetTypeLayout);
}
configurationViews.add(repositoryConfigurationView);
configurationViews.add(rolloutConfigurationView);
configurationViews.add(authenticationConfigurationView);
configurationViews.add(pollingConfigurationView);
if (customConfigurationViews != null) {

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.
*/
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.
*/

View File

@@ -866,6 +866,16 @@ public final class UIComponentIdProvider {
*/
public static final String ROLLOUT_RUN_BUTTON_ID = ROLLOUT_ACTION_ID + ".6";
/**
* Rollout approval button id.
*/
public static final String ROLLOUT_APPROVAL_BUTTON_ID = ROLLOUT_ACTION_ID + ".11";
/**
* Rollout approve/deny option button group id.
*/
public static final String ROLLOUT_APPROVAL_OPTIONGROUP_ID = ROLLOUT_ACTION_ID + ".12";
/**
* Rollout pause button id.
*/
@@ -1082,6 +1092,17 @@ public final class UIComponentIdProvider {
*/
public static final String REPOSITORY_ACTIONS_AUTOCLOSE_CHECKBOX = "repositoryactionsautoclosecheckbox";
/**
* Configuration checkbox for
* {@link TenantConfigurationKey#ROLLOUT_APPROVAL_ENABLED}
*/
public static final String ROLLOUT_APPROVAL_ENABLED_CHECKBOX = "rollout.approve.enabled.checkbox";
/**
* Id of the rollout approval remark field
*/
public static final String ROLLOUT_APPROVAL_REMARK_FIELD_ID = "rollout.approve.remark";
/**
* /* Private Constructor.
*/

View File

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