Support user consent flow (#1293)
* Introduce user consent flow * Add permissions to confirmation management * rename from consent to confirmation * Reformat code. Remove unused imports. Change and add permission checks when configuring auto-confirmation. * Do not include null values for DDI confirmation base endpoint * fix confirmation required checkbox id * Remove unused import. Fix consume/produce type of new API's. * Change term processing to proceeding when activating user consent flow * Align formatting and extend integration test cases for DMF and DDI. * Extend DMF test cases to consider auto-confirmation * Refactor action management to fix problem of handling action status updates on closed actions. * remove unsupported validation * use new confirmation api for DMF. Extend test cases., * Remove unnecessary fields. * Extend API documentation for DDI and MGMT API. * adapt ddi api docs adoc file * Fixed the duplicate migration version for db files * fix method to support confirmation * Fixed PR comments * Addressed PR comments * Fixed after merge compilation issue * Fixed after merge compilation issue * Fix failing tests in MgmtRolloutResourceTest * Fixed the permissions issue reflected by integration tests * Added back the missing line of code lost during merge * Fix the failing test on Jenkins Signed-off-by: Stanislav Trailov <stanislav.trailov@bosch.io> Signed-off-by: Dimitar Shterev <dimitar.shterev@bosch.io> Signed-off-by: Michael Herdt <Michael.Herdt@bosch.io> Signed-off-by: Shruthi Manavalli Ramanna <shruthimanavalli.ramanna@bosch-si.com> Co-authored-by: Shruthi Manavalli Ramanna <shruthimanavalli.ramanna@bosch-si.com>
This commit is contained in:
@@ -159,6 +159,7 @@ public class SecurityManagedConfiguration {
|
|||||||
static class ControllerSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
|
static class ControllerSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
|
||||||
|
|
||||||
private static final String[] DDI_ANT_MATCHERS = { DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}",
|
private static final String[] DDI_ANT_MATCHERS = { DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}",
|
||||||
|
DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/confirmationBase/**",
|
||||||
DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/deploymentBase/**",
|
DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/deploymentBase/**",
|
||||||
DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/installedBase/**",
|
DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/installedBase/**",
|
||||||
DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/cancelAction/**",
|
DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/cancelAction/**",
|
||||||
|
|||||||
@@ -20,10 +20,23 @@ public enum SpServerError {
|
|||||||
SP_REPO_GENERIC_ERROR("hawkbit.server.error.repo.genericError", "unknown error occurred"),
|
SP_REPO_GENERIC_ERROR("hawkbit.server.error.repo.genericError", "unknown error occurred"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
SP_REPO_ENTITY_ALREADY_EXISTS("hawkbit.server.error.repo.entitiyAlreayExists",
|
SP_REPO_ENTITY_ALREADY_EXISTS("hawkbit.server.error.repo.entitiyAlreayExists",
|
||||||
"The given entity already exists in database"),
|
"The given entity already exists in database"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
SP_REPO_AUTO_CONFIRMATION_ALREADY_ACTIVE("hawkbit.server.error.repo.autoConfAlreadyActive",
|
||||||
|
"Auto confirmation is already active"),
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
SP_CONFIRMATION_FEEDBACK_INVALID("hawkbit.server.confirmation.feedback.invalid",
|
||||||
|
"Confirmation feedback is not valid"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import org.eclipse.hawkbit.api.HostnameResolver;
|
|||||||
import org.eclipse.hawkbit.cache.DownloadIdCache;
|
import org.eclipse.hawkbit.cache.DownloadIdCache;
|
||||||
import org.eclipse.hawkbit.dmf.amqp.api.AmqpSettings;
|
import org.eclipse.hawkbit.dmf.amqp.api.AmqpSettings;
|
||||||
import org.eclipse.hawkbit.repository.ArtifactManagement;
|
import org.eclipse.hawkbit.repository.ArtifactManagement;
|
||||||
|
import org.eclipse.hawkbit.repository.ConfirmationManagement;
|
||||||
import org.eclipse.hawkbit.repository.ControllerManagement;
|
import org.eclipse.hawkbit.repository.ControllerManagement;
|
||||||
import org.eclipse.hawkbit.repository.DeploymentManagement;
|
import org.eclipse.hawkbit.repository.DeploymentManagement;
|
||||||
import org.eclipse.hawkbit.repository.DistributionSetManagement;
|
import org.eclipse.hawkbit.repository.DistributionSetManagement;
|
||||||
@@ -275,9 +276,10 @@ public class AmqpConfiguration {
|
|||||||
final AmqpMessageDispatcherService amqpMessageDispatcherService,
|
final AmqpMessageDispatcherService amqpMessageDispatcherService,
|
||||||
final ControllerManagement controllerManagement, final EntityFactory entityFactory,
|
final ControllerManagement controllerManagement, final EntityFactory entityFactory,
|
||||||
final SystemSecurityContext systemSecurityContext,
|
final SystemSecurityContext systemSecurityContext,
|
||||||
final TenantConfigurationManagement tenantConfigurationManagement) {
|
final TenantConfigurationManagement tenantConfigurationManagement,
|
||||||
|
final ConfirmationManagement confirmationManagement) {
|
||||||
return new AmqpMessageHandlerService(rabbitTemplate, amqpMessageDispatcherService, controllerManagement,
|
return new AmqpMessageHandlerService(rabbitTemplate, amqpMessageDispatcherService, controllerManagement,
|
||||||
entityFactory, systemSecurityContext, tenantConfigurationManagement);
|
entityFactory, systemSecurityContext, tenantConfigurationManagement, confirmationManagement);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import org.eclipse.hawkbit.dmf.json.model.DmfActionRequest;
|
|||||||
import org.eclipse.hawkbit.dmf.json.model.DmfArtifact;
|
import org.eclipse.hawkbit.dmf.json.model.DmfArtifact;
|
||||||
import org.eclipse.hawkbit.dmf.json.model.DmfArtifactHash;
|
import org.eclipse.hawkbit.dmf.json.model.DmfArtifactHash;
|
||||||
import org.eclipse.hawkbit.dmf.json.model.DmfBatchDownloadAndUpdateRequest;
|
import org.eclipse.hawkbit.dmf.json.model.DmfBatchDownloadAndUpdateRequest;
|
||||||
|
import org.eclipse.hawkbit.dmf.json.model.DmfConfirmRequest;
|
||||||
import org.eclipse.hawkbit.dmf.json.model.DmfDownloadAndUpdateRequest;
|
import org.eclipse.hawkbit.dmf.json.model.DmfDownloadAndUpdateRequest;
|
||||||
import org.eclipse.hawkbit.dmf.json.model.DmfMetadata;
|
import org.eclipse.hawkbit.dmf.json.model.DmfMetadata;
|
||||||
import org.eclipse.hawkbit.dmf.json.model.DmfMultiActionRequest;
|
import org.eclipse.hawkbit.dmf.json.model.DmfMultiActionRequest;
|
||||||
@@ -273,6 +274,8 @@ public class AmqpMessageDispatcherService extends BaseAmqpService {
|
|||||||
final Map<SoftwareModule, List<SoftwareModuleMetadata>> softwareModules) {
|
final Map<SoftwareModule, List<SoftwareModuleMetadata>> softwareModules) {
|
||||||
if (action.isCancelingOrCanceled()) {
|
if (action.isCancelingOrCanceled()) {
|
||||||
return createPlainActionRequest(action);
|
return createPlainActionRequest(action);
|
||||||
|
} else if (action.isWaitingConfirmation()) {
|
||||||
|
return createConfirmRequest(target, action.getId(), softwareModules);
|
||||||
}
|
}
|
||||||
return createDownloadAndUpdateRequest(target, action.getId(), softwareModules);
|
return createDownloadAndUpdateRequest(target, action.getId(), softwareModules);
|
||||||
}
|
}
|
||||||
@@ -310,6 +313,9 @@ public class AmqpMessageDispatcherService extends BaseAmqpService {
|
|||||||
* @return {@link EventTopic} to use for message.
|
* @return {@link EventTopic} to use for message.
|
||||||
*/
|
*/
|
||||||
private static EventTopic getEventTypeForTarget(final ActionProperties action) {
|
private static EventTopic getEventTypeForTarget(final ActionProperties action) {
|
||||||
|
if (action.isWaitingConfirmation()) {
|
||||||
|
return EventTopic.CONFIRM;
|
||||||
|
}
|
||||||
return (Action.ActionType.DOWNLOAD_ONLY == action.getActionType() || !action.isMaintenanceWindowAvailable())
|
return (Action.ActionType.DOWNLOAD_ONLY == action.getActionType() || !action.isMaintenanceWindowAvailable())
|
||||||
? EventTopic.DOWNLOAD
|
? EventTopic.DOWNLOAD
|
||||||
: EventTopic.DOWNLOAD_AND_INSTALL;
|
: EventTopic.DOWNLOAD_AND_INSTALL;
|
||||||
@@ -415,9 +421,16 @@ public class AmqpMessageDispatcherService extends BaseAmqpService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final DmfDownloadAndUpdateRequest downloadAndUpdateRequest = createDownloadAndUpdateRequest(target,
|
DmfActionRequest request;
|
||||||
action.getId(), modules);
|
if (action.isWaitingConfirmation()) {
|
||||||
final Message message = getMessageConverter().toMessage(downloadAndUpdateRequest,
|
// For the moment the confirmation request is the same as download and update request.
|
||||||
|
// It can be modified not to expose all the software modules in the future.
|
||||||
|
request = createConfirmRequest(target, action.getId(), modules);
|
||||||
|
} else {
|
||||||
|
request = createDownloadAndUpdateRequest(target, action.getId(), modules);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Message message = getMessageConverter().toMessage(request,
|
||||||
createConnectorMessagePropertiesEvent(tenant, target.getControllerId(), getEventTypeForTarget(action)));
|
createConnectorMessagePropertiesEvent(tenant, target.getControllerId(), getEventTypeForTarget(action)));
|
||||||
amqpSenderService.sendMessage(message, targetAddress);
|
amqpSenderService.sendMessage(message, targetAddress);
|
||||||
}
|
}
|
||||||
@@ -624,4 +637,25 @@ public class AmqpMessageDispatcherService extends BaseAmqpService {
|
|||||||
? EventTopic.BATCH_DOWNLOAD
|
? EventTopic.BATCH_DOWNLOAD
|
||||||
: EventTopic.BATCH_DOWNLOAD_AND_INSTALL;
|
: EventTopic.BATCH_DOWNLOAD_AND_INSTALL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Confirmation request.
|
||||||
|
* @param target the target
|
||||||
|
* @param actionId the actionId
|
||||||
|
* @param softwareModules the software modules
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
protected DmfConfirmRequest createConfirmRequest(final Target target, final Long actionId, final Map<SoftwareModule,
|
||||||
|
List<SoftwareModuleMetadata>> softwareModules) {
|
||||||
|
final DmfConfirmRequest request = new DmfConfirmRequest();
|
||||||
|
request.setActionId(actionId);
|
||||||
|
request.setTargetSecurityToken(systemSecurityContext.runAsSystem(target::getSecurityToken));
|
||||||
|
|
||||||
|
//Software modules can be filtered in the future exposing only the needed.
|
||||||
|
if (softwareModules != null) {
|
||||||
|
softwareModules.entrySet()
|
||||||
|
.forEach(entry -> request.addSoftwareModule(convertToAmqpSoftwareModule(target, entry)));
|
||||||
|
}
|
||||||
|
return request;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,12 +24,15 @@ import java.util.stream.Collectors;
|
|||||||
import org.eclipse.hawkbit.dmf.amqp.api.EventTopic;
|
import org.eclipse.hawkbit.dmf.amqp.api.EventTopic;
|
||||||
import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey;
|
import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey;
|
||||||
import org.eclipse.hawkbit.dmf.amqp.api.MessageType;
|
import org.eclipse.hawkbit.dmf.amqp.api.MessageType;
|
||||||
|
import org.eclipse.hawkbit.dmf.json.model.DmfActionStatus;
|
||||||
import org.eclipse.hawkbit.dmf.json.model.DmfActionUpdateStatus;
|
import org.eclipse.hawkbit.dmf.json.model.DmfActionUpdateStatus;
|
||||||
import org.eclipse.hawkbit.dmf.json.model.DmfAttributeUpdate;
|
import org.eclipse.hawkbit.dmf.json.model.DmfAttributeUpdate;
|
||||||
|
import org.eclipse.hawkbit.dmf.json.model.DmfAutoConfirmation;
|
||||||
import org.eclipse.hawkbit.dmf.json.model.DmfCreateThing;
|
import org.eclipse.hawkbit.dmf.json.model.DmfCreateThing;
|
||||||
import org.eclipse.hawkbit.dmf.json.model.DmfUpdateMode;
|
import org.eclipse.hawkbit.dmf.json.model.DmfUpdateMode;
|
||||||
import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions;
|
import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions;
|
||||||
import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails;
|
import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails;
|
||||||
|
import org.eclipse.hawkbit.repository.ConfirmationManagement;
|
||||||
import org.eclipse.hawkbit.repository.ControllerManagement;
|
import org.eclipse.hawkbit.repository.ControllerManagement;
|
||||||
import org.eclipse.hawkbit.repository.EntityFactory;
|
import org.eclipse.hawkbit.repository.EntityFactory;
|
||||||
import org.eclipse.hawkbit.repository.RepositoryConstants;
|
import org.eclipse.hawkbit.repository.RepositoryConstants;
|
||||||
@@ -75,6 +78,7 @@ public class AmqpMessageHandlerService extends BaseAmqpService {
|
|||||||
private final AmqpMessageDispatcherService amqpMessageDispatcherService;
|
private final AmqpMessageDispatcherService amqpMessageDispatcherService;
|
||||||
|
|
||||||
private ControllerManagement controllerManagement;
|
private ControllerManagement controllerManagement;
|
||||||
|
private ConfirmationManagement confirmationManagement;
|
||||||
|
|
||||||
private final EntityFactory entityFactory;
|
private final EntityFactory entityFactory;
|
||||||
|
|
||||||
@@ -101,18 +105,21 @@ public class AmqpMessageHandlerService extends BaseAmqpService {
|
|||||||
* the system Security Context
|
* the system Security Context
|
||||||
* @param tenantConfigurationManagement
|
* @param tenantConfigurationManagement
|
||||||
* the tenant configuration Management
|
* the tenant configuration Management
|
||||||
|
* @param confirmationManagement
|
||||||
|
* the confirmation management
|
||||||
*/
|
*/
|
||||||
public AmqpMessageHandlerService(final RabbitTemplate rabbitTemplate,
|
public AmqpMessageHandlerService(final RabbitTemplate rabbitTemplate,
|
||||||
final AmqpMessageDispatcherService amqpMessageDispatcherService,
|
final AmqpMessageDispatcherService amqpMessageDispatcherService,
|
||||||
final ControllerManagement controllerManagement, final EntityFactory entityFactory,
|
final ControllerManagement controllerManagement, final EntityFactory entityFactory,
|
||||||
final SystemSecurityContext systemSecurityContext,
|
final SystemSecurityContext systemSecurityContext,
|
||||||
final TenantConfigurationManagement tenantConfigurationManagement) {
|
final TenantConfigurationManagement tenantConfigurationManagement, final ConfirmationManagement confirmationManagement) {
|
||||||
super(rabbitTemplate);
|
super(rabbitTemplate);
|
||||||
this.amqpMessageDispatcherService = amqpMessageDispatcherService;
|
this.amqpMessageDispatcherService = amqpMessageDispatcherService;
|
||||||
this.controllerManagement = controllerManagement;
|
this.controllerManagement = controllerManagement;
|
||||||
this.entityFactory = entityFactory;
|
this.entityFactory = entityFactory;
|
||||||
this.systemSecurityContext = systemSecurityContext;
|
this.systemSecurityContext = systemSecurityContext;
|
||||||
this.tenantConfigurationManagement = tenantConfigurationManagement;
|
this.tenantConfigurationManagement = tenantConfigurationManagement;
|
||||||
|
this.confirmationManagement = confirmationManagement;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -314,6 +321,9 @@ public class AmqpMessageHandlerService extends BaseAmqpService {
|
|||||||
case UPDATE_ATTRIBUTES:
|
case UPDATE_ATTRIBUTES:
|
||||||
updateAttributes(message);
|
updateAttributes(message);
|
||||||
break;
|
break;
|
||||||
|
case UPDATE_AUTO_CONFIRM:
|
||||||
|
setAutoConfirmationState(message);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
logAndThrowMessageError(message, "Got event without appropriate topic.");
|
logAndThrowMessageError(message, "Got event without appropriate topic.");
|
||||||
break;
|
break;
|
||||||
@@ -331,7 +341,23 @@ public class AmqpMessageHandlerService extends BaseAmqpService {
|
|||||||
final String thingId = getStringHeaderKey(message, MessageHeaderKey.THING_ID, THING_ID_NULL);
|
final String thingId = getStringHeaderKey(message, MessageHeaderKey.THING_ID, THING_ID_NULL);
|
||||||
|
|
||||||
controllerManagement.updateControllerAttributes(thingId, attributeUpdate.getAttributes(),
|
controllerManagement.updateControllerAttributes(thingId, attributeUpdate.getAttributes(),
|
||||||
getUpdateMode(attributeUpdate));
|
getUpdateMode(attributeUpdate));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setAutoConfirmationState(final Message message) {
|
||||||
|
final DmfAutoConfirmation autoConfirmation = convertMessage(message, DmfAutoConfirmation.class);
|
||||||
|
final String thingId = getStringHeaderKey(message, MessageHeaderKey.THING_ID, THING_ID_NULL);
|
||||||
|
if (autoConfirmation.isEnabled()) {
|
||||||
|
LOG.debug("Activate auto-confirmation for device {} using DMF. Initiator: {}. Remark: {}", thingId,
|
||||||
|
autoConfirmation.getInitiator(), autoConfirmation.getRemark());
|
||||||
|
final String remark = autoConfirmation.getRemark() == null
|
||||||
|
? "Activated using Device Management Federation API."
|
||||||
|
: autoConfirmation.getRemark();
|
||||||
|
controllerManagement.activateAutoConfirmation(thingId, autoConfirmation.getInitiator(), remark);
|
||||||
|
} else {
|
||||||
|
LOG.debug("Deactivate auto-confirmation for device {} using DMF.", thingId);
|
||||||
|
controllerManagement.deactivateAutoConfirmation(thingId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -352,19 +378,27 @@ public class AmqpMessageHandlerService extends BaseAmqpService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final Status status = mapStatus(message, actionUpdateStatus, action);
|
final Status status = mapStatus(message, actionUpdateStatus, action);
|
||||||
final ActionStatusCreate actionStatus = entityFactory.actionStatus().create(action.getId()).status(status)
|
|
||||||
.messages(messages);
|
|
||||||
|
|
||||||
actionUpdateStatus.getCode().ifPresent(code -> {
|
final Action updatedAction;
|
||||||
actionStatus.code(code);
|
|
||||||
actionStatus.message("Device reported status code: " + code);
|
|
||||||
});
|
|
||||||
|
|
||||||
final Action updatedAction = (Status.CANCELED == status)
|
if (actionUpdateStatus.getActionStatus() == DmfActionStatus.CONFIRMED) {
|
||||||
? controllerManagement.addCancelActionStatus(actionStatus)
|
updatedAction = confirmationManagement.confirmAction(action.getId(),
|
||||||
: controllerManagement.addUpdateActionStatus(actionStatus);
|
actionUpdateStatus.getCode().orElse(null), messages);
|
||||||
|
} else if (actionUpdateStatus.getActionStatus() == DmfActionStatus.DENIED) {
|
||||||
|
updatedAction = confirmationManagement.denyAction(action.getId(), actionUpdateStatus.getCode().orElse(null),
|
||||||
|
messages);
|
||||||
|
} else {
|
||||||
|
final ActionStatusCreate actionStatus = entityFactory.actionStatus().create(action.getId()).status(status)
|
||||||
|
.messages(messages);
|
||||||
|
actionUpdateStatus.getCode().ifPresent(code -> {
|
||||||
|
actionStatus.code(code);
|
||||||
|
actionStatus.message("Device reported status code: " + code);
|
||||||
|
});
|
||||||
|
updatedAction = (Status.CANCELED == status) ? controllerManagement.addCancelActionStatus(actionStatus)
|
||||||
|
: controllerManagement.addUpdateActionStatus(actionStatus);
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldTargetProceed(updatedAction)) {
|
if (shouldTargetProceed(updatedAction) || actionUpdateStatus.getActionStatus() == DmfActionStatus.CONFIRMED) {
|
||||||
sendUpdateCommandToTarget(action.getTarget());
|
sendUpdateCommandToTarget(action.getTarget());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -391,6 +425,7 @@ public class AmqpMessageHandlerService extends BaseAmqpService {
|
|||||||
status = Status.RETRIEVED;
|
status = Status.RETRIEVED;
|
||||||
break;
|
break;
|
||||||
case RUNNING:
|
case RUNNING:
|
||||||
|
case CONFIRMED:
|
||||||
status = Status.RUNNING;
|
status = Status.RUNNING;
|
||||||
break;
|
break;
|
||||||
case CANCELED:
|
case CANCELED:
|
||||||
@@ -411,6 +446,9 @@ public class AmqpMessageHandlerService extends BaseAmqpService {
|
|||||||
case CANCEL_REJECTED:
|
case CANCEL_REJECTED:
|
||||||
status = handleCancelRejectedState(message, action);
|
status = handleCancelRejectedState(message, action);
|
||||||
break;
|
break;
|
||||||
|
case DENIED:
|
||||||
|
status = Status.WAIT_FOR_CONFIRMATION;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
logAndThrowMessageError(message, "Status for action does not exisit.");
|
logAndThrowMessageError(message, "Status for action does not exisit.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey;
|
|||||||
import org.eclipse.hawkbit.dmf.amqp.api.MessageType;
|
import org.eclipse.hawkbit.dmf.amqp.api.MessageType;
|
||||||
import org.eclipse.hawkbit.dmf.json.model.DmfDownloadResponse;
|
import org.eclipse.hawkbit.dmf.json.model.DmfDownloadResponse;
|
||||||
import org.eclipse.hawkbit.repository.ArtifactManagement;
|
import org.eclipse.hawkbit.repository.ArtifactManagement;
|
||||||
|
import org.eclipse.hawkbit.repository.ConfirmationManagement;
|
||||||
import org.eclipse.hawkbit.repository.ControllerManagement;
|
import org.eclipse.hawkbit.repository.ControllerManagement;
|
||||||
import org.eclipse.hawkbit.repository.SystemManagement;
|
import org.eclipse.hawkbit.repository.SystemManagement;
|
||||||
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
|
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
|
||||||
@@ -117,6 +118,9 @@ public class AmqpControllerAuthenticationTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private ControllerManagement controllerManagement;
|
private ControllerManagement controllerManagement;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ConfirmationManagement confirmationManagement;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private DdiSecurityProperties securityProperties;
|
private DdiSecurityProperties securityProperties;
|
||||||
|
|
||||||
@@ -156,7 +160,7 @@ public class AmqpControllerAuthenticationTest {
|
|||||||
|
|
||||||
amqpMessageHandlerService = new AmqpMessageHandlerService(rabbitTemplate,
|
amqpMessageHandlerService = new AmqpMessageHandlerService(rabbitTemplate,
|
||||||
mock(AmqpMessageDispatcherService.class), controllerManagement, new JpaEntityFactory(),
|
mock(AmqpMessageDispatcherService.class), controllerManagement, new JpaEntityFactory(),
|
||||||
systemSecurityContext, tenantConfigurationManagementMock);
|
systemSecurityContext, tenantConfigurationManagementMock, confirmationManagement);
|
||||||
|
|
||||||
amqpAuthenticationMessageHandlerService = new AmqpAuthenticationMessageHandler(rabbitTemplate,
|
amqpAuthenticationMessageHandlerService = new AmqpAuthenticationMessageHandler(rabbitTemplate,
|
||||||
authenticationManager, artifactManagementMock, cacheMock, hostnameResolverMock, controllerManagement,
|
authenticationManager, artifactManagementMock, cacheMock, hostnameResolverMock, controllerManagement,
|
||||||
|
|||||||
@@ -36,16 +36,19 @@ import org.eclipse.hawkbit.dmf.amqp.api.MessageType;
|
|||||||
import org.eclipse.hawkbit.dmf.json.model.DmfActionStatus;
|
import org.eclipse.hawkbit.dmf.json.model.DmfActionStatus;
|
||||||
import org.eclipse.hawkbit.dmf.json.model.DmfActionUpdateStatus;
|
import org.eclipse.hawkbit.dmf.json.model.DmfActionUpdateStatus;
|
||||||
import org.eclipse.hawkbit.dmf.json.model.DmfAttributeUpdate;
|
import org.eclipse.hawkbit.dmf.json.model.DmfAttributeUpdate;
|
||||||
|
import org.eclipse.hawkbit.dmf.json.model.DmfAutoConfirmation;
|
||||||
import org.eclipse.hawkbit.dmf.json.model.DmfCreateThing;
|
import org.eclipse.hawkbit.dmf.json.model.DmfCreateThing;
|
||||||
import org.eclipse.hawkbit.dmf.json.model.DmfDownloadResponse;
|
import org.eclipse.hawkbit.dmf.json.model.DmfDownloadResponse;
|
||||||
import org.eclipse.hawkbit.dmf.json.model.DmfUpdateMode;
|
import org.eclipse.hawkbit.dmf.json.model.DmfUpdateMode;
|
||||||
import org.eclipse.hawkbit.repository.ArtifactManagement;
|
import org.eclipse.hawkbit.repository.ArtifactManagement;
|
||||||
|
import org.eclipse.hawkbit.repository.ConfirmationManagement;
|
||||||
import org.eclipse.hawkbit.repository.ControllerManagement;
|
import org.eclipse.hawkbit.repository.ControllerManagement;
|
||||||
import org.eclipse.hawkbit.repository.EntityFactory;
|
import org.eclipse.hawkbit.repository.EntityFactory;
|
||||||
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
|
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
|
||||||
import org.eclipse.hawkbit.repository.UpdateMode;
|
import org.eclipse.hawkbit.repository.UpdateMode;
|
||||||
import org.eclipse.hawkbit.repository.builder.ActionStatusBuilder;
|
import org.eclipse.hawkbit.repository.builder.ActionStatusBuilder;
|
||||||
import org.eclipse.hawkbit.repository.builder.ActionStatusCreate;
|
import org.eclipse.hawkbit.repository.builder.ActionStatusCreate;
|
||||||
|
import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
|
||||||
import org.eclipse.hawkbit.repository.jpa.builder.JpaActionStatusBuilder;
|
import org.eclipse.hawkbit.repository.jpa.builder.JpaActionStatusBuilder;
|
||||||
import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus;
|
import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus;
|
||||||
import org.eclipse.hawkbit.repository.jpa.model.helper.SecurityTokenGeneratorHolder;
|
import org.eclipse.hawkbit.repository.jpa.model.helper.SecurityTokenGeneratorHolder;
|
||||||
@@ -110,6 +113,9 @@ public class AmqpMessageHandlerServiceTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private ControllerManagement controllerManagementMock;
|
private ControllerManagement controllerManagementMock;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ConfirmationManagement confirmationManagementMock;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private EntityFactory entityFactoryMock;
|
private EntityFactory entityFactoryMock;
|
||||||
|
|
||||||
@@ -146,6 +152,12 @@ public class AmqpMessageHandlerServiceTest {
|
|||||||
@Captor
|
@Captor
|
||||||
private ArgumentCaptor<String> targetIdCaptor;
|
private ArgumentCaptor<String> targetIdCaptor;
|
||||||
|
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<String> initiatorCaptor;
|
||||||
|
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<String> remarkCaptor;
|
||||||
|
|
||||||
@Captor
|
@Captor
|
||||||
private ArgumentCaptor<String> targetNameCaptor;
|
private ArgumentCaptor<String> targetNameCaptor;
|
||||||
|
|
||||||
@@ -169,7 +181,8 @@ public class AmqpMessageHandlerServiceTest {
|
|||||||
final SystemSecurityContext systemSecurityContext = new SystemSecurityContext(tenantAware);
|
final SystemSecurityContext systemSecurityContext = new SystemSecurityContext(tenantAware);
|
||||||
|
|
||||||
amqpMessageHandlerService = new AmqpMessageHandlerService(rabbitTemplate, amqpMessageDispatcherServiceMock,
|
amqpMessageHandlerService = new AmqpMessageHandlerService(rabbitTemplate, amqpMessageDispatcherServiceMock,
|
||||||
controllerManagementMock, entityFactoryMock, systemSecurityContext, tenantConfigurationManagement);
|
controllerManagementMock, entityFactoryMock, systemSecurityContext, tenantConfigurationManagement,
|
||||||
|
confirmationManagementMock);
|
||||||
amqpAuthenticationMessageHandlerService = new AmqpAuthenticationMessageHandler(rabbitTemplate,
|
amqpAuthenticationMessageHandlerService = new AmqpAuthenticationMessageHandler(rabbitTemplate,
|
||||||
authenticationManagerMock, artifactManagementMock, downloadIdCache, hostnameResolverMock,
|
authenticationManagerMock, artifactManagementMock, downloadIdCache, hostnameResolverMock,
|
||||||
controllerManagementMock, tenantAwareMock);
|
controllerManagementMock, tenantAwareMock);
|
||||||
@@ -230,6 +243,16 @@ public class AmqpMessageHandlerServiceTest {
|
|||||||
assertThat(uriCaptor.getValue()).as("Uri is not right").hasToString("amqp://" + VIRTUAL_HOST + "/" + replyTo);
|
assertThat(uriCaptor.getValue()).as("Uri is not right").hasToString("amqp://" + VIRTUAL_HOST + "/" + replyTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Step
|
||||||
|
private void assertInitiatorCapturedField(final String initiator) {
|
||||||
|
assertThat(initiatorCaptor.getValue()).as("Initiator is wrong").isEqualTo(initiator);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Step
|
||||||
|
private void assertRemarkCapturedField(final String remark) {
|
||||||
|
assertThat(remarkCaptor.getValue()).as("Remark is wrong").isEqualTo(remark);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Description("Tests the creation of a target/thing with specified name by calling the same method that incoming RabbitMQ messages would access.")
|
@Description("Tests the creation of a target/thing with specified name by calling the same method that incoming RabbitMQ messages would access.")
|
||||||
public void createThingWithName() {
|
public void createThingWithName() {
|
||||||
@@ -556,6 +579,7 @@ public class AmqpMessageHandlerServiceTest {
|
|||||||
final ActionStatusCreate create = mock(ActionStatusCreate.class);
|
final ActionStatusCreate create = mock(ActionStatusCreate.class);
|
||||||
when(builder.create(22L)).thenReturn(create);
|
when(builder.create(22L)).thenReturn(create);
|
||||||
when(create.status(any())).thenReturn(create);
|
when(create.status(any())).thenReturn(create);
|
||||||
|
when(create.messages(any())).thenReturn(create);
|
||||||
when(entityFactoryMock.actionStatus()).thenReturn(builder);
|
when(entityFactoryMock.actionStatus()).thenReturn(builder);
|
||||||
// for the test the same action can be used
|
// for the test the same action can be used
|
||||||
when(controllerManagementMock.findActiveActionWithHighestWeight(any())).thenReturn(Optional.of(action));
|
when(controllerManagementMock.findActiveActionWithHighestWeight(any())).thenReturn(Optional.of(action));
|
||||||
@@ -572,7 +596,7 @@ public class AmqpMessageHandlerServiceTest {
|
|||||||
final ArgumentCaptor<Target> targetCaptor = ArgumentCaptor.forClass(Target.class);
|
final ArgumentCaptor<Target> targetCaptor = ArgumentCaptor.forClass(Target.class);
|
||||||
|
|
||||||
verify(amqpMessageDispatcherServiceMock, times(1)).sendUpdateMessageToTarget(actionPropertiesCaptor.capture(),
|
verify(amqpMessageDispatcherServiceMock, times(1)).sendUpdateMessageToTarget(actionPropertiesCaptor.capture(),
|
||||||
targetCaptor.capture(), any(Map.class));
|
targetCaptor.capture(), any(Map.class));
|
||||||
final ActionProperties actionProperties = actionPropertiesCaptor.getValue();
|
final ActionProperties actionProperties = actionPropertiesCaptor.getValue();
|
||||||
assertThat(actionProperties).isNotNull();
|
assertThat(actionProperties).isNotNull();
|
||||||
assertThat(actionProperties.getTenant()).as("event has tenant").isEqualTo("DEFAULT");
|
assertThat(actionProperties.getTenant()).as("event has tenant").isEqualTo("DEFAULT");
|
||||||
@@ -616,6 +640,56 @@ public class AmqpMessageHandlerServiceTest {
|
|||||||
.contains("Device reported status code: 12");
|
.contains("Device reported status code: 12");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Tests activating auto-confirmation on a target.")
|
||||||
|
void setAutoConfirmationStateActive() {
|
||||||
|
final String knownThingId = "1";
|
||||||
|
final String initiator = "iAmTheInitiator";
|
||||||
|
final String remark = "remarkForTesting";
|
||||||
|
|
||||||
|
final MessageProperties messageProperties = createMessageProperties(MessageType.EVENT);
|
||||||
|
messageProperties.setHeader(MessageHeaderKey.THING_ID, knownThingId);
|
||||||
|
messageProperties.setHeader(MessageHeaderKey.TOPIC, "UPDATE_AUTO_CONFIRM");
|
||||||
|
final DmfAutoConfirmation autoConfirmation = new DmfAutoConfirmation();
|
||||||
|
autoConfirmation.setEnabled(true);
|
||||||
|
autoConfirmation.setInitiator(initiator);
|
||||||
|
autoConfirmation.setRemark(remark);
|
||||||
|
|
||||||
|
final Message message = createMessage(autoConfirmation, messageProperties);
|
||||||
|
|
||||||
|
when(controllerManagementMock.activateAutoConfirmation(targetIdCaptor.capture(), initiatorCaptor.capture(),
|
||||||
|
remarkCaptor.capture())).thenReturn(null);
|
||||||
|
|
||||||
|
amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, VIRTUAL_HOST);
|
||||||
|
|
||||||
|
verify(controllerManagementMock, times(0)).deactivateAutoConfirmation(anyString());
|
||||||
|
|
||||||
|
assertThingIdCapturedField(knownThingId);
|
||||||
|
assertInitiatorCapturedField(initiator);
|
||||||
|
assertRemarkCapturedField(remark);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Tests deactivating auto-confirmation on a target.")
|
||||||
|
void setAutoConfirmationStateDeactivated() {
|
||||||
|
final String knownThingId = "1";
|
||||||
|
|
||||||
|
final MessageProperties messageProperties = createMessageProperties(MessageType.EVENT);
|
||||||
|
messageProperties.setHeader(MessageHeaderKey.THING_ID, knownThingId);
|
||||||
|
messageProperties.setHeader(MessageHeaderKey.TOPIC, "UPDATE_AUTO_CONFIRM");
|
||||||
|
final DmfAutoConfirmation autoConfirmation = new DmfAutoConfirmation();
|
||||||
|
|
||||||
|
final Message message = createMessage(autoConfirmation, messageProperties);
|
||||||
|
|
||||||
|
amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, VIRTUAL_HOST);
|
||||||
|
|
||||||
|
verify(controllerManagementMock).deactivateAutoConfirmation(targetIdCaptor.capture());
|
||||||
|
verify(controllerManagementMock, times(0)).activateAutoConfirmation(anyString(), anyString(), anyString());
|
||||||
|
|
||||||
|
assertThingIdCapturedField(knownThingId);
|
||||||
|
}
|
||||||
|
|
||||||
private DmfActionUpdateStatus createActionUpdateStatus(final DmfActionStatus status) {
|
private DmfActionUpdateStatus createActionUpdateStatus(final DmfActionStatus status) {
|
||||||
return createActionUpdateStatus(status, 2L);
|
return createActionUpdateStatus(status, 2L);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -31,6 +30,7 @@ import org.eclipse.hawkbit.dmf.json.model.DmfActionRequest;
|
|||||||
import org.eclipse.hawkbit.dmf.json.model.DmfActionStatus;
|
import org.eclipse.hawkbit.dmf.json.model.DmfActionStatus;
|
||||||
import org.eclipse.hawkbit.dmf.json.model.DmfActionUpdateStatus;
|
import org.eclipse.hawkbit.dmf.json.model.DmfActionUpdateStatus;
|
||||||
import org.eclipse.hawkbit.dmf.json.model.DmfAttributeUpdate;
|
import org.eclipse.hawkbit.dmf.json.model.DmfAttributeUpdate;
|
||||||
|
import org.eclipse.hawkbit.dmf.json.model.DmfConfirmRequest;
|
||||||
import org.eclipse.hawkbit.dmf.json.model.DmfCreateThing;
|
import org.eclipse.hawkbit.dmf.json.model.DmfCreateThing;
|
||||||
import org.eclipse.hawkbit.dmf.json.model.DmfDownloadAndUpdateRequest;
|
import org.eclipse.hawkbit.dmf.json.model.DmfDownloadAndUpdateRequest;
|
||||||
import org.eclipse.hawkbit.dmf.json.model.DmfMetadata;
|
import org.eclipse.hawkbit.dmf.json.model.DmfMetadata;
|
||||||
@@ -129,7 +129,13 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt
|
|||||||
testdataFactory.addSoftwareModuleMetadata(distributionSet);
|
testdataFactory.addSoftwareModuleMetadata(distributionSet);
|
||||||
|
|
||||||
return registerTargetAndAssignDistributionSet(distributionSet.getId(), TargetUpdateStatus.REGISTERED,
|
return registerTargetAndAssignDistributionSet(distributionSet.getId(), TargetUpdateStatus.REGISTERED,
|
||||||
distributionSet.getModules(), controllerId);
|
distributionSet.getModules(), controllerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected DistributionSetAssignmentResult prepareDistributionSetAndAssign(final String controllerId) {
|
||||||
|
distributionSet = testdataFactory.createDistributionSet(UUID.randomUUID().toString());
|
||||||
|
testdataFactory.addSoftwareModuleMetadata(distributionSet);
|
||||||
|
return assignDistributionSet(distributionSet.getId(), controllerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected DistributionSetAssignmentResult registerTargetAndAssignDistributionSet(final Long assignDs,
|
protected DistributionSetAssignmentResult registerTargetAndAssignDistributionSet(final Long assignDs,
|
||||||
@@ -138,7 +144,11 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt
|
|||||||
registerAndAssertTargetWithExistingTenant(controllerId, 1, expectedStatus, CREATED_BY);
|
registerAndAssertTargetWithExistingTenant(controllerId, 1, expectedStatus, CREATED_BY);
|
||||||
|
|
||||||
final DistributionSetAssignmentResult assignmentResult = assignDistributionSet(assignDs, controllerId);
|
final DistributionSetAssignmentResult assignmentResult = assignDistributionSet(assignDs, controllerId);
|
||||||
assertDownloadAndInstallMessage(expectedSoftwareModulesInMessage, controllerId);
|
if (isConfirmationFlowEnabled()) {
|
||||||
|
assertConfirmMessage(expectedSoftwareModulesInMessage, controllerId);
|
||||||
|
} else {
|
||||||
|
assertDownloadAndInstallMessage(expectedSoftwareModulesInMessage, controllerId);
|
||||||
|
}
|
||||||
return assignmentResult;
|
return assignmentResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,4 +462,24 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt
|
|||||||
softwareModules.forEach(dmfModule -> assertThat(dmfModule.getMetadata()).containsExactly(
|
softwareModules.forEach(dmfModule -> assertThat(dmfModule.getMetadata()).containsExactly(
|
||||||
new DmfMetadata(TestdataFactory.VISIBLE_SM_MD_KEY, TestdataFactory.VISIBLE_SM_MD_VALUE)));
|
new DmfMetadata(TestdataFactory.VISIBLE_SM_MD_KEY, TestdataFactory.VISIBLE_SM_MD_VALUE)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void assertConfirmMessage(final Set<SoftwareModule> dsModules, final String controllerId) {
|
||||||
|
|
||||||
|
final Message replyMessage = assertReplyMessageHeader(EventTopic.CONFIRM, controllerId);
|
||||||
|
assertAllTargetsCount(1);
|
||||||
|
|
||||||
|
final DmfConfirmRequest confirmRequest = (DmfConfirmRequest) getDmfClient()
|
||||||
|
.getMessageConverter().fromMessage(replyMessage);
|
||||||
|
|
||||||
|
assertConfirmRequest(confirmRequest, dsModules, controllerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void assertConfirmRequest(final DmfConfirmRequest request, final Set<SoftwareModule> softwareModules,
|
||||||
|
final String controllerId) {
|
||||||
|
assertSoftwareModules(softwareModules, request.getSoftwareModules());
|
||||||
|
final Target updatedTarget = waitUntilIsPresent(() -> targetManagement.getByControllerID(controllerId));
|
||||||
|
assertThat(updatedTarget).isNotNull();
|
||||||
|
assertThat(updatedTarget.getSecurityToken()).isEqualTo(request.getTargetSecurityToken());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -466,7 +466,7 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer
|
|||||||
|
|
||||||
private Rollout createAndStartRollout(final DistributionSet ds, final String filterQuery, final Integer weight) {
|
private Rollout createAndStartRollout(final DistributionSet ds, final String filterQuery, final Integer weight) {
|
||||||
final Rollout rollout = testdataFactory.createRolloutByVariables(UUID.randomUUID().toString(), "", 1,
|
final Rollout rollout = testdataFactory.createRolloutByVariables(UUID.randomUUID().toString(), "", 1,
|
||||||
filterQuery, ds, "50", "5", ActionType.FORCED, weight);
|
filterQuery, ds, "50", "5", ActionType.FORCED, weight, false);
|
||||||
rolloutManagement.start(rollout.getId());
|
rolloutManagement.start(rollout.getId());
|
||||||
rolloutManagement.handleRollouts();
|
rolloutManagement.handleRollouts();
|
||||||
return rollout;
|
return rollout;
|
||||||
@@ -695,4 +695,28 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer
|
|||||||
assertThat(tokens.contains(requestTarget.getTargetSecurityToken()));
|
assertThat(tokens.contains(requestTarget.getTargetSecurityToken()));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Verify that a distribution assignment send a confirm message.")
|
||||||
|
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
|
||||||
|
@Expect(type = ActionCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||||
|
@Expect(type = SoftwareModuleUpdatedEvent.class, count = 6),
|
||||||
|
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = TargetUpdatedEvent.class, count = 1),
|
||||||
|
@Expect(type = TargetPollEvent.class, count = 1),
|
||||||
|
@Expect(type = TenantConfigurationCreatedEvent.class, count = 1) })
|
||||||
|
void sendConfirmStatus() {
|
||||||
|
final String controllerId = TARGET_PREFIX + "sendConfirmStatus";
|
||||||
|
enableConfirmationFlow();
|
||||||
|
registerTargetAndAssignDistributionSet(controllerId);
|
||||||
|
|
||||||
|
waitUntilTargetHasStatus(controllerId, TargetUpdateStatus.PENDING);
|
||||||
|
assertConfirmMessage(getDistributionSet().getModules(), controllerId);
|
||||||
|
assertEventMessageNotPresent(EventTopic.DOWNLOAD_AND_INSTALL);
|
||||||
|
}
|
||||||
|
protected void assertEventMessageNotPresent(final EventTopic eventTopic) {
|
||||||
|
assertThat(replyToListener.getLatestEventMessage(eventTopic)).isNull();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedE
|
|||||||
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleUpdatedEvent;
|
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleUpdatedEvent;
|
||||||
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
|
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
|
||||||
import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
|
import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
|
||||||
|
import org.eclipse.hawkbit.repository.event.remote.entity.TenantConfigurationCreatedEvent;
|
||||||
|
import org.eclipse.hawkbit.repository.event.remote.entity.TenantConfigurationUpdatedEvent;
|
||||||
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
|
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
|
||||||
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
|
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
|
||||||
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
|
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
|
||||||
@@ -966,6 +968,26 @@ class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServiceIntegr
|
|||||||
getDmfClient().send(updateMessage);
|
getDmfClient().send(updateMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void assertActionStatusList(final Long actionId, final int statusListCount,
|
||||||
|
final Status... expectedActionStates) {
|
||||||
|
createConditionFactory().await().untilAsserted(() -> {
|
||||||
|
try {
|
||||||
|
WithSpringAuthorityRule.runAsPrivileged(() -> {
|
||||||
|
final List<ActionStatus> actionStatusList = deploymentManagement
|
||||||
|
.findActionStatusByAction(PAGE, actionId).getContent();
|
||||||
|
assertThat(actionStatusList).hasSize(statusListCount);
|
||||||
|
|
||||||
|
final List<Status> status = actionStatusList.stream().map(ActionStatus::getStatus)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
assertThat(status).containsOnly(expectedActionStates);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
} catch (final Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
private int getAuthenticationMessageCount() {
|
private int getAuthenticationMessageCount() {
|
||||||
return Integer
|
return Integer
|
||||||
.parseInt(Objects.requireNonNull(getRabbitAdmin().getQueueProperties(amqpProperties.getReceiverQueue()))
|
.parseInt(Objects.requireNonNull(getRabbitAdmin().getQueueProperties(amqpProperties.getReceiverQueue()))
|
||||||
@@ -993,4 +1015,142 @@ class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServiceIntegr
|
|||||||
assertThat(node.has("actionId")).isTrue();
|
assertThat(node.has("actionId")).isTrue();
|
||||||
return node.get("actionId").asText();
|
return node.get("actionId").asText();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Register a target and send a update action status (confirmed). Verify if the updated action status is correct.")
|
||||||
|
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
|
||||||
|
@Expect(type = ActionUpdatedEvent.class, count = 1),
|
||||||
|
@Expect(type = ActionCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||||
|
@Expect(type = SoftwareModuleUpdatedEvent.class, count = 6),
|
||||||
|
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = TargetUpdatedEvent.class, count = 1),
|
||||||
|
@Expect(type = TargetPollEvent.class, count = 1),
|
||||||
|
@Expect(type = TenantConfigurationCreatedEvent.class, count = 1) })
|
||||||
|
void confirmedActionStatus() {
|
||||||
|
enableConfirmationFlow();
|
||||||
|
final String controllerId = TARGET_PREFIX + "confirmedActionStatus";
|
||||||
|
|
||||||
|
final DistributionSetAssignmentResult assignmentResult = registerTargetAndAssignDistributionSet(controllerId);
|
||||||
|
final Long actionId = getFirstAssignedActionId(assignmentResult);
|
||||||
|
createAndSendActionStatusUpdateMessage(controllerId, actionId, DmfActionStatus.CONFIRMED);
|
||||||
|
assertActionStatusList(actionId, 2, Status.WAIT_FOR_CONFIRMATION, Status.RUNNING);
|
||||||
|
|
||||||
|
// assert download and install message
|
||||||
|
waitUntilEventMessagesAreDispatchedToTarget(EventTopic.CONFIRM, EventTopic.DOWNLOAD_AND_INSTALL);
|
||||||
|
assertDownloadAndInstallMessage(assignmentResult.getDistributionSet().getModules(), controllerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Verify the DMF confirmed feedback can be provided if confirmation flow is disabled")
|
||||||
|
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
|
||||||
|
@Expect(type = ActionUpdatedEvent.class, count = 1),
|
||||||
|
@Expect(type = ActionCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||||
|
@Expect(type = SoftwareModuleUpdatedEvent.class, count = 6),
|
||||||
|
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = TargetUpdatedEvent.class, count = 1),
|
||||||
|
@Expect(type = TargetPollEvent.class, count = 1),
|
||||||
|
@Expect(type = TenantConfigurationCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = TenantConfigurationUpdatedEvent.class, count = 1) })
|
||||||
|
void verifyActionCanBeConfirmedOnDisabledConfirmationFlow() {
|
||||||
|
enableConfirmationFlow();
|
||||||
|
final String controllerId = TARGET_PREFIX + "confirmedActionStatus";
|
||||||
|
|
||||||
|
final DistributionSetAssignmentResult assignmentResult = registerTargetAndAssignDistributionSet(controllerId);
|
||||||
|
final Long actionId = getFirstAssignedActionId(assignmentResult);
|
||||||
|
// verify action status is in WAIT_FOR_CONFIRMATION
|
||||||
|
assertActionStatusList(actionId, 1, Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
|
||||||
|
disableConfirmationFlow();
|
||||||
|
|
||||||
|
createAndSendActionStatusUpdateMessage(controllerId, actionId, DmfActionStatus.CONFIRMED);
|
||||||
|
assertActionStatusList(actionId, 2, Status.WAIT_FOR_CONFIRMATION, Status.RUNNING);
|
||||||
|
|
||||||
|
// assert download and install message
|
||||||
|
waitUntilEventMessagesAreDispatchedToTarget(EventTopic.CONFIRM, EventTopic.DOWNLOAD_AND_INSTALL);
|
||||||
|
assertDownloadAndInstallMessage(assignmentResult.getDistributionSet().getModules(), controllerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Verify the DMF confirmed feedback can be provided if confirmation flow is disabled")
|
||||||
|
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
|
||||||
|
@Expect(type = ActionUpdatedEvent.class, count = 0),
|
||||||
|
@Expect(type = ActionCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||||
|
@Expect(type = SoftwareModuleUpdatedEvent.class, count = 6),
|
||||||
|
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = TargetUpdatedEvent.class, count = 1),
|
||||||
|
@Expect(type = TargetPollEvent.class, count = 1),
|
||||||
|
@Expect(type = TenantConfigurationCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = TenantConfigurationUpdatedEvent.class, count = 1)})
|
||||||
|
void verifyActionCanBeDeniedOnDisabledConfirmationFlow() {
|
||||||
|
enableConfirmationFlow();
|
||||||
|
final String controllerId = TARGET_PREFIX + "confirmedActionStatus";
|
||||||
|
|
||||||
|
final DistributionSetAssignmentResult assignmentResult = registerTargetAndAssignDistributionSet(controllerId);
|
||||||
|
final Long actionId = getFirstAssignedActionId(assignmentResult);
|
||||||
|
// verify action status is in WAIT_FOR_CONFIRMATION
|
||||||
|
assertActionStatusList(actionId, 1, Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
|
||||||
|
disableConfirmationFlow();
|
||||||
|
|
||||||
|
createAndSendActionStatusUpdateMessage(controllerId, actionId, DmfActionStatus.DENIED);
|
||||||
|
assertActionStatusList(actionId, 2, Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Verify the DMF download and install message is send directly if auto-confirmation is active")
|
||||||
|
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
|
||||||
|
@Expect(type = ActionUpdatedEvent.class, count = 0),
|
||||||
|
@Expect(type = ActionCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||||
|
@Expect(type = SoftwareModuleUpdatedEvent.class, count = 6),
|
||||||
|
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = TargetUpdatedEvent.class, count = 2),
|
||||||
|
@Expect(type = TargetPollEvent.class, count = 1),
|
||||||
|
@Expect(type = TenantConfigurationCreatedEvent.class, count = 1) })
|
||||||
|
void verifyDownloadAndInstallDirectlySendOnAutoConfirmationEnabled() {
|
||||||
|
enableConfirmationFlow();
|
||||||
|
final String controllerId = TARGET_PREFIX + "confirmedActionStatus";
|
||||||
|
|
||||||
|
registerAndAssertTargetWithExistingTenant(controllerId);
|
||||||
|
|
||||||
|
confirmationManagement.activateAutoConfirmation(controllerId, null, null);
|
||||||
|
|
||||||
|
final DistributionSetAssignmentResult assignmentResult = prepareDistributionSetAndAssign(controllerId);
|
||||||
|
final Long actionId = getFirstAssignedActionId(assignmentResult);
|
||||||
|
// verify action status is in WAIT_FOR_CONFIRMATION
|
||||||
|
assertActionStatusList(actionId, 1, Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
|
||||||
|
// assert download and install message
|
||||||
|
waitUntilEventMessagesAreDispatchedToTarget(EventTopic.DOWNLOAD_AND_INSTALL);
|
||||||
|
assertDownloadAndInstallMessage(assignmentResult.getDistributionSet().getModules(), controllerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Register a target and send a update action status (denied). Verify if the updated action status is correct.")
|
||||||
|
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
|
||||||
|
@Expect(type = ActionUpdatedEvent.class), @Expect(type = ActionCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||||
|
@Expect(type = SoftwareModuleUpdatedEvent.class, count = 6),
|
||||||
|
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = TargetUpdatedEvent.class, count = 1),
|
||||||
|
@Expect(type = TargetPollEvent.class, count = 1),
|
||||||
|
@Expect(type = TenantConfigurationCreatedEvent.class, count = 1) })
|
||||||
|
void deniedActionStatus() {
|
||||||
|
enableConfirmationFlow();
|
||||||
|
final String controllerId = TARGET_PREFIX + "deniedActionStatus";
|
||||||
|
final DistributionSetAssignmentResult assignmentResult = registerTargetAndAssignDistributionSet(controllerId);
|
||||||
|
final Long actionId = getFirstAssignedActionId(assignmentResult);
|
||||||
|
assertActionStatusList(actionId, 1, Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
createAndSendActionStatusUpdateMessage(controllerId, actionId, DmfActionStatus.DENIED);
|
||||||
|
assertActionStatusList(actionId, 2, Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ public enum EventTopic {
|
|||||||
*/
|
*/
|
||||||
UPDATE_ATTRIBUTES,
|
UPDATE_ATTRIBUTES,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Topic when updating auto-confirmation state.
|
||||||
|
*/
|
||||||
|
UPDATE_AUTO_CONFIRM,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Topic when sending a download only task, skipping the install.
|
* Topic when sending a download only task, skipping the install.
|
||||||
*/
|
*/
|
||||||
@@ -56,6 +61,10 @@ public enum EventTopic {
|
|||||||
/**
|
/**
|
||||||
* Topic when sending a download and install action to multiple devices.
|
* Topic when sending a download and install action to multiple devices.
|
||||||
*/
|
*/
|
||||||
BATCH_DOWNLOAD_AND_INSTALL
|
BATCH_DOWNLOAD_AND_INSTALL,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Topic when confirmation of an action is requested.
|
||||||
|
*/
|
||||||
|
CONFIRM
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,5 +64,15 @@ public enum DmfActionStatus {
|
|||||||
/**
|
/**
|
||||||
* Action has been downloaded for this target.
|
* Action has been downloaded for this target.
|
||||||
*/
|
*/
|
||||||
DOWNLOADED
|
DOWNLOADED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action is confirmed by the target.
|
||||||
|
*/
|
||||||
|
CONFIRMED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action has been denied by the target.
|
||||||
|
*/
|
||||||
|
DENIED
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ public class DmfActionUpdateStatus {
|
|||||||
private Long softwareModuleId;
|
private Long softwareModuleId;
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private Integer code;
|
private List<String> message;
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private List<String> message;
|
private Integer code;
|
||||||
|
|
||||||
public DmfActionUpdateStatus(@JsonProperty(value = "actionId", required = true) final Long actionId,
|
public DmfActionUpdateStatus(@JsonProperty(value = "actionId", required = true) final Long actionId,
|
||||||
@JsonProperty(value = "actionStatus", required = true) final DmfActionStatus actionStatus) {
|
@JsonProperty(value = "actionStatus", required = true) final DmfActionStatus actionStatus) {
|
||||||
@@ -99,4 +99,8 @@ public class DmfActionUpdateStatus {
|
|||||||
return message.addAll(messages);
|
return message.addAll(messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setCode(final int code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2022 Bosch.IO 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.dmf.json.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON representation of auto confirmation config.
|
||||||
|
*/
|
||||||
|
@JsonInclude(Include.NON_NULL)
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class DmfAutoConfirmation {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private boolean enabled;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String initiator;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(final boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getInitiator() {
|
||||||
|
return initiator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInitiator(final String initiator) {
|
||||||
|
this.initiator = initiator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRemark() {
|
||||||
|
return remark;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRemark(final String remark) {
|
||||||
|
this.remark = remark;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2022 Bosch.IO 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.dmf.json.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON representation of confirm request.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@JsonInclude(Include.NON_NULL)
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class DmfConfirmRequest extends DmfActionRequest {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String targetSecurityToken;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private List<DmfSoftwareModule> softwareModules;
|
||||||
|
|
||||||
|
public String getTargetSecurityToken() {
|
||||||
|
return targetSecurityToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTargetSecurityToken(final String targetSecurityToken) {
|
||||||
|
this.targetSecurityToken = targetSecurityToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DmfSoftwareModule> getSoftwareModules() {
|
||||||
|
if (softwareModules == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Collections.unmodifiableList(softwareModules);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a Software module.
|
||||||
|
*
|
||||||
|
* @param createSoftwareModule
|
||||||
|
* the module
|
||||||
|
*/
|
||||||
|
public void addSoftwareModule(final DmfSoftwareModule createSoftwareModule) {
|
||||||
|
if (softwareModules == null) {
|
||||||
|
softwareModules = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
softwareModules.add(createSoftwareModule);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2022 Bosch.IO 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.im.authentication.SpPermission;
|
||||||
|
import org.eclipse.hawkbit.repository.model.Action;
|
||||||
|
import org.eclipse.hawkbit.repository.model.AutoConfirmationStatus;
|
||||||
|
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotEmpty;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service layer for all confirmation related operations.
|
||||||
|
*/
|
||||||
|
public interface ConfirmationManagement {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find active actions in the {@link Action.Status#WAIT_FOR_CONFIRMATION} state
|
||||||
|
* for a specific target with a specified controllerId.
|
||||||
|
*
|
||||||
|
* @param controllerId
|
||||||
|
* of the target to check
|
||||||
|
* @return a list of {@link Action}
|
||||||
|
*/
|
||||||
|
@PreAuthorize(SpPermission.SpringEvalExpressions.HAS_AUTH_READ_TARGET)
|
||||||
|
List<Action> findActiveActionsWaitingConfirmation(@NotEmpty String controllerId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate auto confirmation for a given controller ID. In case auto
|
||||||
|
* confirmation is active already, this method will fail with an exception.
|
||||||
|
*
|
||||||
|
* @param controllerId
|
||||||
|
* to activate the feature for
|
||||||
|
* @param initiator
|
||||||
|
* who initiated this operation. If 'null' we will take the current
|
||||||
|
* user from {@link TenantAware#getCurrentUsername()}
|
||||||
|
* @param remark
|
||||||
|
* optional field to set a remark
|
||||||
|
* @return the persisted {@link AutoConfirmationStatus}
|
||||||
|
*/
|
||||||
|
@PreAuthorize(SpPermission.SpringEvalExpressions.IS_CONTROLLER_OR_HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET)
|
||||||
|
AutoConfirmationStatus activateAutoConfirmation(@NotEmpty String controllerId, final String initiator,
|
||||||
|
final String remark);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current state of auto-confirmation for a given controllerId
|
||||||
|
*
|
||||||
|
* @param controllerId
|
||||||
|
* to check the state for
|
||||||
|
* @return instance of {@link AutoConfirmationStatus} wrapped in an
|
||||||
|
* {@link Optional}. Present if active and empty if disabled.
|
||||||
|
*/
|
||||||
|
@PreAuthorize(SpPermission.SpringEvalExpressions.HAS_AUTH_READ_TARGET)
|
||||||
|
Optional<AutoConfirmationStatus> getStatus(@NotEmpty String controllerId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto confirm active actions for a specific controller ID having the
|
||||||
|
* {@link Action.Status#WAIT_FOR_CONFIRMATION} status.
|
||||||
|
*
|
||||||
|
* @param controllerId
|
||||||
|
* to confirm actions for
|
||||||
|
* @return a list of confirmed actions
|
||||||
|
*/
|
||||||
|
@PreAuthorize(SpPermission.SpringEvalExpressions.IS_CONTROLLER_OR_HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET)
|
||||||
|
List<Action> autoConfirmActiveActions(@NotEmpty String controllerId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm a given action to put it from
|
||||||
|
* {@link Action.Status#WAIT_FOR_CONFIRMATION} to {@link Action.Status#RUNNING}
|
||||||
|
* state.
|
||||||
|
*
|
||||||
|
* @param actionId
|
||||||
|
* mandatory to know which action to confirm
|
||||||
|
* @param code
|
||||||
|
* optional value to specify a code for the created action status
|
||||||
|
* @param messages
|
||||||
|
* optional value to specify message for the created action status
|
||||||
|
*/
|
||||||
|
@PreAuthorize(SpPermission.SpringEvalExpressions.IS_CONTROLLER_OR_HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET)
|
||||||
|
Action confirmAction(long actionId, Integer code, Collection<String> messages);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deny a given action and leave it in
|
||||||
|
* {@link Action.Status#WAIT_FOR_CONFIRMATION} state.
|
||||||
|
*
|
||||||
|
* @param actionId
|
||||||
|
* mandatory to know which action to deny
|
||||||
|
* @param code
|
||||||
|
* optional value to specify a code for the created action status
|
||||||
|
* @param messages
|
||||||
|
* optional value to specify message for the created action status
|
||||||
|
*/
|
||||||
|
@PreAuthorize(SpPermission.SpringEvalExpressions.IS_CONTROLLER_OR_HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET)
|
||||||
|
Action denyAction(long actionId, Integer code, Collection<String> messages);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deactivate auto confirmation for a specific controller id
|
||||||
|
*
|
||||||
|
* @param controllerId
|
||||||
|
* to disable auto confirmation for
|
||||||
|
*/
|
||||||
|
@PreAuthorize(SpPermission.SpringEvalExpressions.IS_CONTROLLER_OR_HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET)
|
||||||
|
void deactivateAutoConfirmation(@NotEmpty String controllerId);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ import org.eclipse.hawkbit.repository.exception.InvalidTargetAttributeException;
|
|||||||
import org.eclipse.hawkbit.repository.model.Action;
|
import org.eclipse.hawkbit.repository.model.Action;
|
||||||
import org.eclipse.hawkbit.repository.model.Action.Status;
|
import org.eclipse.hawkbit.repository.model.Action.Status;
|
||||||
import org.eclipse.hawkbit.repository.model.ActionStatus;
|
import org.eclipse.hawkbit.repository.model.ActionStatus;
|
||||||
|
import org.eclipse.hawkbit.repository.model.AutoConfirmationStatus;
|
||||||
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
||||||
import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata;
|
import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata;
|
||||||
import org.eclipse.hawkbit.repository.model.Target;
|
import org.eclipse.hawkbit.repository.model.Target;
|
||||||
@@ -508,4 +509,28 @@ public interface ControllerManagement {
|
|||||||
*/
|
*/
|
||||||
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
|
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
|
||||||
Optional<Action> getInstalledActionByTarget(@NotEmpty String controllerId);
|
Optional<Action> getInstalledActionByTarget(@NotEmpty String controllerId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate auto confirmation for a given controllerId
|
||||||
|
*
|
||||||
|
* @param controllerId
|
||||||
|
* to activate auto-confirmation on
|
||||||
|
* @param initiator
|
||||||
|
* can be set optionally (fallback is the current acting security
|
||||||
|
* user)
|
||||||
|
* @param remark
|
||||||
|
* (optional) remark
|
||||||
|
* @return the persisted {@link AutoConfirmationStatus}
|
||||||
|
*/
|
||||||
|
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
|
||||||
|
AutoConfirmationStatus activateAutoConfirmation(@NotEmpty String controllerId, String initiator, String remark);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deactivate auto confirmation for a given controllerId
|
||||||
|
*
|
||||||
|
* @param controllerId
|
||||||
|
* to deactivate auto-confirmation on
|
||||||
|
*/
|
||||||
|
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
|
||||||
|
void deactivateAutoConfirmation(@NotEmpty String controllerId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -449,8 +449,8 @@ public interface DeploymentManagement {
|
|||||||
Page<Action> findInActiveActionsByTarget(@NotNull Pageable pageable, @NotEmpty String controllerId);
|
Page<Action> findInActiveActionsByTarget(@NotNull Pageable pageable, @NotEmpty String controllerId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves active {@link Action}s with highest weight that are assigned to
|
* Retrieves active {@link Action}s with highest weight that are assigned to a
|
||||||
* a {@link Target}.
|
* {@link Target}.
|
||||||
*
|
*
|
||||||
* @param controllerId
|
* @param controllerId
|
||||||
* identifies the target to retrieve the action from
|
* identifies the target to retrieve the action from
|
||||||
@@ -522,6 +522,8 @@ public interface DeploymentManagement {
|
|||||||
*
|
*
|
||||||
* @param rolloutId
|
* @param rolloutId
|
||||||
* the rollout the actions belong to
|
* the rollout the actions belong to
|
||||||
|
* @param distributionSetId
|
||||||
|
* to assign
|
||||||
* @param rolloutGroupParentId
|
* @param rolloutGroupParentId
|
||||||
* the parent rollout group the actions should reference. null
|
* the parent rollout group the actions should reference. null
|
||||||
* references the first group
|
* references the first group
|
||||||
|
|||||||
@@ -108,27 +108,28 @@ public interface RolloutManagement {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Persists a new rollout entity. The filter within the
|
* Persists a new rollout entity. The filter within the
|
||||||
* {@link Rollout#getTargetFilterQuery()} is used to retrieve the targets
|
* {@link Rollout#getTargetFilterQuery()} is used to retrieve the targets which
|
||||||
* which are effected by this rollout to create. The amount of groups will
|
* are effected by this rollout to create. The amount of groups will be defined
|
||||||
* be defined as equally sized.
|
* as equally sized.
|
||||||
*
|
*
|
||||||
* The rollout is not started. Only the preparation of the rollout is done,
|
* The rollout is not started. Only the preparation of the rollout is done,
|
||||||
* creating and persisting all the necessary groups. The Rollout and the
|
* creating and persisting all the necessary groups. The Rollout and the groups
|
||||||
* groups are persisted in {@link RolloutStatus#CREATING} and
|
* are persisted in {@link RolloutStatus#CREATING} and
|
||||||
* {@link RolloutGroupStatus#CREATING}.
|
* {@link RolloutGroupStatus#CREATING}.
|
||||||
*
|
*
|
||||||
* The RolloutScheduler will start to assign targets to the groups. Once all
|
* The RolloutScheduler will start to assign targets to the groups. Once all
|
||||||
* targets have been assigned to the groups, the rollout status is changed
|
* targets have been assigned to the groups, the rollout status is changed to
|
||||||
* to {@link RolloutStatus#READY} so it can be started with
|
* {@link RolloutStatus#READY} so it can be started with .
|
||||||
* {@link #start(Rollout)}.
|
|
||||||
*
|
*
|
||||||
* @param create
|
* @param create
|
||||||
* the rollout entity to create
|
* the rollout entity to create
|
||||||
* @param amountGroup
|
* @param amountGroup
|
||||||
* the amount of groups to split the rollout into
|
* the amount of groups to split the rollout into
|
||||||
|
* @param confirmationRequired
|
||||||
|
* if a confirmation is required by the device group(s) of the rollout
|
||||||
* @param conditions
|
* @param conditions
|
||||||
* the rolloutgroup conditions and actions which should be
|
* the rolloutgroup conditions and actions which should be applied
|
||||||
* applied for each {@link RolloutGroup}
|
* for each {@link RolloutGroup}
|
||||||
* @return the persisted rollout.
|
* @return the persisted rollout.
|
||||||
*
|
*
|
||||||
* @throws EntityNotFoundException
|
* @throws EntityNotFoundException
|
||||||
@@ -140,22 +141,23 @@ public interface RolloutManagement {
|
|||||||
* exceeded.
|
* exceeded.
|
||||||
*/
|
*/
|
||||||
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_CREATE)
|
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_CREATE)
|
||||||
Rollout create(@NotNull @Valid RolloutCreate create, int amountGroup, @NotNull RolloutGroupConditions conditions);
|
Rollout create(@NotNull @Valid RolloutCreate create, int amountGroup, boolean confirmationRequired,
|
||||||
|
@NotNull RolloutGroupConditions conditions);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persists a new rollout entity. The filter within the
|
* Persists a new rollout entity. The filter within the
|
||||||
* {@link Rollout#getTargetFilterQuery()} is used to filter the targets
|
* {@link Rollout#getTargetFilterQuery()} is used to filter the targets which
|
||||||
* which are affected by this rollout. The given groups will be used to
|
* are affected by this rollout. The given groups will be used to create the
|
||||||
* create the groups.
|
* groups.
|
||||||
*
|
*
|
||||||
* The rollout is not started. Only the preparation of the rollout is done,
|
* The rollout is not started. Only the preparation of the rollout is done,
|
||||||
* creating and persisting all the necessary groups. The Rollout and the
|
* creating and persisting all the necessary groups. The Rollout and the groups
|
||||||
* groups are persisted in {@link RolloutStatus#CREATING} and
|
* are persisted in {@link RolloutStatus#CREATING} and
|
||||||
* {@link RolloutGroupStatus#CREATING}.
|
* {@link RolloutGroupStatus#CREATING}.
|
||||||
*
|
*
|
||||||
* The RolloutScheduler will start to assign targets to the groups. Once all
|
* The RolloutScheduler will start to assign targets to the groups. Once all
|
||||||
* targets have been assigned to the groups, the rollout status is changed
|
* targets have been assigned to the groups, the rollout status is changed to
|
||||||
* to {@link RolloutStatus#READY} so it can be started with
|
* {@link RolloutStatus#READY} so it can be started with
|
||||||
* {@link #start(Rollout)}.
|
* {@link #start(Rollout)}.
|
||||||
*
|
*
|
||||||
* @param rollout
|
* @param rollout
|
||||||
@@ -163,9 +165,9 @@ public interface RolloutManagement {
|
|||||||
* @param groups
|
* @param groups
|
||||||
* optional definition of groups
|
* optional definition of groups
|
||||||
* @param conditions
|
* @param conditions
|
||||||
* the rollout group conditions and actions which should be
|
* the rollout group conditions and actions which should be applied
|
||||||
* applied for each {@link RolloutGroup} if not defined by the
|
* for each {@link RolloutGroup} if not defined by the RolloutGroup
|
||||||
* RolloutGroup itself
|
* itself
|
||||||
* @return the persisted rollout.
|
* @return the persisted rollout.
|
||||||
*
|
*
|
||||||
* @throws EntityNotFoundException
|
* @throws EntityNotFoundException
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ public class AutoAssignDistributionSetUpdate {
|
|||||||
@Max(Action.WEIGHT_MAX)
|
@Max(Action.WEIGHT_MAX)
|
||||||
private Integer weight;
|
private Integer weight;
|
||||||
|
|
||||||
|
private Boolean confirmationRequired;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*
|
*
|
||||||
@@ -75,6 +77,19 @@ public class AutoAssignDistributionSetUpdate {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify initial confirmation state of resulting {@link Action}
|
||||||
|
*
|
||||||
|
* @param confirmationRequired
|
||||||
|
* if confirmation is required for this auto assignment (considered
|
||||||
|
* with confirmation flow active)
|
||||||
|
* @return updated builder instance
|
||||||
|
*/
|
||||||
|
public AutoAssignDistributionSetUpdate confirmationRequired(final boolean confirmationRequired) {
|
||||||
|
this.confirmationRequired = confirmationRequired;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Long getDsId() {
|
public Long getDsId() {
|
||||||
return dsId;
|
return dsId;
|
||||||
}
|
}
|
||||||
@@ -87,6 +102,10 @@ public class AutoAssignDistributionSetUpdate {
|
|||||||
return weight;
|
return weight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean isConfirmationRequired() {
|
||||||
|
return confirmationRequired;
|
||||||
|
}
|
||||||
|
|
||||||
public long getTargetFilterId() {
|
public long getTargetFilterId() {
|
||||||
return targetFilterId;
|
return targetFilterId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,14 @@ public interface RolloutGroupCreate {
|
|||||||
*/
|
*/
|
||||||
RolloutGroupCreate conditions(RolloutGroupConditions conditions);
|
RolloutGroupCreate conditions(RolloutGroupConditions conditions);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param confirmationRequired
|
||||||
|
* if confirmation is required for this rollout group (considered
|
||||||
|
* with confirmation flow active)
|
||||||
|
* @return updated builder instance
|
||||||
|
*/
|
||||||
|
RolloutGroupCreate confirmationRequired(boolean confirmationRequired);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return peek on current state of {@link RolloutGroup} in the builder
|
* @return peek on current state of {@link RolloutGroup} in the builder
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -83,6 +83,17 @@ public interface TargetFilterQueryCreate {
|
|||||||
*/
|
*/
|
||||||
TargetFilterQueryCreate autoAssignWeight(Integer weight);
|
TargetFilterQueryCreate autoAssignWeight(Integer weight);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify initial confirmation state of resulting {@link Action} in auto
|
||||||
|
* assignment
|
||||||
|
*
|
||||||
|
* @param confirmationRequired
|
||||||
|
* if confirmation is required for configured auto assignment (considered
|
||||||
|
* with confirmation flow active)
|
||||||
|
* @return updated builder instance
|
||||||
|
*/
|
||||||
|
TargetFilterQueryCreate confirmationRequired(boolean confirmationRequired);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return peek on current state of {@link TargetFilterQuery} in the builder
|
* @return peek on current state of {@link TargetFilterQuery} in the builder
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2022 Bosch.IO 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.exception;
|
||||||
|
|
||||||
|
import org.eclipse.hawkbit.exception.AbstractServerRtException;
|
||||||
|
import org.eclipse.hawkbit.exception.SpServerError;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link AutoConfirmationAlreadyActiveException} is thrown when auto
|
||||||
|
* confirmation is already active for a device but the
|
||||||
|
* {@link org.eclipse.hawkbit.repository.ConfirmationManagement#activateAutoConfirmation}
|
||||||
|
* is getting called.
|
||||||
|
*/
|
||||||
|
public class AutoConfirmationAlreadyActiveException extends AbstractServerRtException {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
private static final SpServerError THIS_ERROR = SpServerError.SP_REPO_AUTO_CONFIRMATION_ALREADY_ACTIVE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default constructor.
|
||||||
|
*/
|
||||||
|
public AutoConfirmationAlreadyActiveException() {
|
||||||
|
super(THIS_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameterized constructor.
|
||||||
|
*
|
||||||
|
* @param cause
|
||||||
|
* of the exception
|
||||||
|
*/
|
||||||
|
public AutoConfirmationAlreadyActiveException(final Throwable cause) {
|
||||||
|
super(THIS_ERROR, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameterized constructor for auto confirmation is already active for given
|
||||||
|
* controller ID
|
||||||
|
*
|
||||||
|
* @param controllerId
|
||||||
|
* of affected device
|
||||||
|
*/
|
||||||
|
public AutoConfirmationAlreadyActiveException(final String controllerId) {
|
||||||
|
super("Auto confirmation is already active for device " + controllerId, THIS_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2022 Bosch.IO 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.exception;
|
||||||
|
|
||||||
|
import org.eclipse.hawkbit.exception.AbstractServerRtException;
|
||||||
|
import org.eclipse.hawkbit.exception.SpServerError;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This exception is indicating that the confirmation feedback cannot be
|
||||||
|
* processed for a specific actions for different reasons which are listed as
|
||||||
|
* enum {@link Reason}.
|
||||||
|
*/
|
||||||
|
public class InvalidConfirmationFeedbackException extends AbstractServerRtException {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
private static final SpServerError THIS_ERROR = SpServerError.SP_CONFIRMATION_FEEDBACK_INVALID;
|
||||||
|
|
||||||
|
private final Reason reason;
|
||||||
|
|
||||||
|
protected InvalidConfirmationFeedbackException(final Reason reason) {
|
||||||
|
super(THIS_ERROR);
|
||||||
|
this.reason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InvalidConfirmationFeedbackException(final Reason reason, final String message) {
|
||||||
|
super(message, THIS_ERROR);
|
||||||
|
this.reason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Reason getReason() {
|
||||||
|
return reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Reason {
|
||||||
|
ACTION_CLOSED, NOT_AWAITING_CONFIRMATION
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -259,7 +259,12 @@ public interface Action extends TenantAwareBaseEntity {
|
|||||||
* Action has been downloaded by the target and waiting for update to
|
* Action has been downloaded by the target and waiting for update to
|
||||||
* start.
|
* start.
|
||||||
*/
|
*/
|
||||||
DOWNLOADED
|
DOWNLOADED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action is waiting to be confirmed by the user
|
||||||
|
*/
|
||||||
|
WAIT_FOR_CONFIRMATION
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -330,4 +335,11 @@ public interface Action extends TenantAwareBaseEntity {
|
|||||||
* @return true if maintenance window is available, else false.
|
* @return true if maintenance window is available, else false.
|
||||||
*/
|
*/
|
||||||
boolean isMaintenanceWindowAvailable();
|
boolean isMaintenanceWindowAvailable();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the action is waiting for confirmation.
|
||||||
|
* @return true if the action is waiting for confirmation, else false
|
||||||
|
*/
|
||||||
|
boolean isWaitingConfirmation();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
*/
|
*/
|
||||||
package org.eclipse.hawkbit.repository.model;
|
package org.eclipse.hawkbit.repository.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,6 +24,8 @@ public class ActionProperties implements Serializable {
|
|||||||
private String tenant;
|
private String tenant;
|
||||||
private boolean maintenanceWindowAvailable;
|
private boolean maintenanceWindowAvailable;
|
||||||
|
|
||||||
|
private Action.Status status;
|
||||||
|
|
||||||
public ActionProperties() {
|
public ActionProperties() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +39,7 @@ public class ActionProperties implements Serializable {
|
|||||||
this.actionType = action.getActionType();
|
this.actionType = action.getActionType();
|
||||||
this.tenant = action.getTenant();
|
this.tenant = action.getTenant();
|
||||||
this.maintenanceWindowAvailable = action.isMaintenanceWindowAvailable();
|
this.maintenanceWindowAvailable = action.isMaintenanceWindowAvailable();
|
||||||
|
this.status = action.getStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setId(final Long id) {
|
public void setId(final Long id) {
|
||||||
@@ -68,4 +73,13 @@ public class ActionProperties implements Serializable {
|
|||||||
public void setActionType(final Action.ActionType actionType) {
|
public void setActionType(final Action.ActionType actionType) {
|
||||||
this.actionType = actionType;
|
this.actionType = actionType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Action.Status getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public boolean isWaitingConfirmation() {
|
||||||
|
return status == Action.Status.WAIT_FOR_CONFIRMATION;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2022 Bosch.IO 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.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link AutoConfirmationStatus} of a {@link Target}.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public interface AutoConfirmationStatus extends BaseEntity {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For which target this status is corresponding to.
|
||||||
|
*
|
||||||
|
* @return the {@link Target}
|
||||||
|
*/
|
||||||
|
Target getTarget();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user who initiated the auto confirmation. Will be set on auto
|
||||||
|
* confirmation activation and could be null. In this case the created_by can be
|
||||||
|
* considered as initiator.
|
||||||
|
*
|
||||||
|
* @return the user
|
||||||
|
*/
|
||||||
|
String getInitiator();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unix timestamp of the activation.
|
||||||
|
*
|
||||||
|
* @return activation time as unix timestamp
|
||||||
|
*/
|
||||||
|
long getActivatedAt();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional value, which can be set during activation.
|
||||||
|
*
|
||||||
|
* @return the remark
|
||||||
|
*/
|
||||||
|
String getRemark();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct the action message based on the current status.
|
||||||
|
*
|
||||||
|
* @return the constructed message which can be used for the action status as a
|
||||||
|
* message
|
||||||
|
*/
|
||||||
|
String constructActionMessage();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -25,8 +25,8 @@ public class DeploymentRequest {
|
|||||||
private final TargetWithActionType targetWithActionType;
|
private final TargetWithActionType targetWithActionType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor that also accepts maintenance schedule parameters and checks
|
* Constructor that also accepts maintenance schedule parameters and checks for
|
||||||
* for validity of the specified maintenance schedule.
|
* validity of the specified maintenance schedule.
|
||||||
*
|
*
|
||||||
* @param controllerId
|
* @param controllerId
|
||||||
* for which the action is created.
|
* for which the action is created.
|
||||||
@@ -48,18 +48,30 @@ public class DeploymentRequest {
|
|||||||
* window, for example 00:30:00 for 30 minutes
|
* window, for example 00:30:00 for 30 minutes
|
||||||
* @param maintenanceWindowTimeZone
|
* @param maintenanceWindowTimeZone
|
||||||
* is the time zone specified as +/-hh:mm offset from UTC, for
|
* is the time zone specified as +/-hh:mm offset from UTC, for
|
||||||
* example +02:00 for CET summer time and +00:00 for UTC. The
|
* example +02:00 for CET summer time and +00:00 for UTC. The start
|
||||||
* start time of a maintenance window calculated based on the
|
* time of a maintenance window calculated based on the cron
|
||||||
* cron expression is relative to this time zone.
|
* expression is relative to this time zone.
|
||||||
*
|
*
|
||||||
|
* @param confirmationRequired
|
||||||
|
* is a flag whether the confirmation should be required for the
|
||||||
|
* resulting {@link Action} or not. In case the confirmation is not
|
||||||
|
* required, the action will be automatically confirmed and put in
|
||||||
|
* the
|
||||||
|
* {@link org.eclipse.hawkbit.repository.model.Action.Status#RUNNING}
|
||||||
|
* state. Otherwise the confirmation flow will be triggered
|
||||||
|
* and the {@link Action} will stay in the
|
||||||
|
* {@link org.eclipse.hawkbit.repository.model.Action.Status#WAIT_FOR_CONFIRMATION}
|
||||||
|
* state until the confirmation is given. (Only considered
|
||||||
|
* with CONFIRMATION_FLOW active via tenant configuration)
|
||||||
* @throws InvalidMaintenanceScheduleException
|
* @throws InvalidMaintenanceScheduleException
|
||||||
* if the parameters do not define a valid maintenance schedule.
|
* if the parameters do not define a valid maintenance schedule.
|
||||||
*/
|
*/
|
||||||
public DeploymentRequest(final String controllerId, final Long distributionSetId, final ActionType actionType,
|
public DeploymentRequest(final String controllerId, final Long distributionSetId, final ActionType actionType,
|
||||||
final long forceTime, final Integer weight, final String maintenanceSchedule,
|
final long forceTime, final Integer weight, final String maintenanceSchedule,
|
||||||
final String maintenanceWindowDuration, final String maintenanceWindowTimeZone) {
|
final String maintenanceWindowDuration, final String maintenanceWindowTimeZone,
|
||||||
|
final boolean confirmationRequired) {
|
||||||
this.targetWithActionType = new TargetWithActionType(controllerId, actionType, forceTime, weight,
|
this.targetWithActionType = new TargetWithActionType(controllerId, actionType, forceTime, weight,
|
||||||
maintenanceSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone);
|
maintenanceSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone, confirmationRequired);
|
||||||
this.distributionSetId = distributionSetId;
|
this.distributionSetId = distributionSetId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,11 +90,11 @@ public class DeploymentRequest {
|
|||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return String.format(
|
return String.format(
|
||||||
"DeploymentRequest [controllerId=%s, distributionSetId=%d, actionType=%s, forceTime=%d, weight=%d, maintenanceSchedule=%s, maintenanceWindowDuration=%s, maintenanceWindowTimeZone=%s]",
|
"DeploymentRequest [controllerId=%s, distributionSetId=%d, actionType=%s, forceTime=%d, weight=%d, maintenanceSchedule=%s, maintenanceWindowDuration=%s, maintenanceWindowTimeZone=%s, confirmationRequired=%s]",
|
||||||
targetWithActionType.getControllerId(), getDistributionSetId(), targetWithActionType.getActionType(),
|
targetWithActionType.getControllerId(), getDistributionSetId(), targetWithActionType.getActionType(),
|
||||||
targetWithActionType.getForceTime(), targetWithActionType.getWeight(),
|
targetWithActionType.getForceTime(), targetWithActionType.getWeight(),
|
||||||
targetWithActionType.getMaintenanceSchedule(), targetWithActionType.getMaintenanceWindowDuration(),
|
targetWithActionType.getMaintenanceSchedule(), targetWithActionType.getMaintenanceWindowDuration(),
|
||||||
targetWithActionType.getMaintenanceWindowTimeZone());
|
targetWithActionType.getMaintenanceWindowTimeZone(), targetWithActionType.isConfirmationRequired());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public class DeploymentRequestBuilder {
|
|||||||
private String maintenanceSchedule;
|
private String maintenanceSchedule;
|
||||||
private String maintenanceWindowDuration;
|
private String maintenanceWindowDuration;
|
||||||
private String maintenanceWindowTimeZone;
|
private String maintenanceWindowTimeZone;
|
||||||
|
private boolean confirmationRequired;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a builder for a target distribution set assignment with the
|
* Create a builder for a target distribution set assignment with the
|
||||||
@@ -100,6 +101,18 @@ public class DeploymentRequestBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set if a confirmation is required.
|
||||||
|
*
|
||||||
|
* @param confirmationRequired
|
||||||
|
* if a confirmation is required for the {@link Action}
|
||||||
|
* @return builder
|
||||||
|
*/
|
||||||
|
public DeploymentRequestBuilder setConfirmationRequired(final boolean confirmationRequired) {
|
||||||
|
this.confirmationRequired = confirmationRequired;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* build the request
|
* build the request
|
||||||
*
|
*
|
||||||
@@ -107,7 +120,7 @@ public class DeploymentRequestBuilder {
|
|||||||
*/
|
*/
|
||||||
public DeploymentRequest build() {
|
public DeploymentRequest build() {
|
||||||
return new DeploymentRequest(controllerId, distributionSetId, actionType, forceTime, weight,
|
return new DeploymentRequest(controllerId, distributionSetId, actionType, forceTime, weight,
|
||||||
maintenanceSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone);
|
maintenanceSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone, confirmationRequired);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,12 @@ public interface RolloutGroup extends NamedEntity {
|
|||||||
*/
|
*/
|
||||||
float getTargetPercentage();
|
float getTargetPercentage();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return if a confirmation is required for the resulting actions (considered
|
||||||
|
* with confirmation flow active only)
|
||||||
|
*/
|
||||||
|
boolean isConfirmationRequired();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rollout group state machine.
|
* Rollout group state machine.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -98,6 +98,14 @@ public interface Target extends NamedEntity {
|
|||||||
*/
|
*/
|
||||||
PollStatus getPollStatus();
|
PollStatus getPollStatus();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The auto confirmation status is present, when it's active for the target.
|
||||||
|
* Will only be considered in case the confirmation flow is active.
|
||||||
|
*
|
||||||
|
* @return the {@link AutoConfirmationStatus} if activated
|
||||||
|
*/
|
||||||
|
AutoConfirmationStatus getAutoConfirmationStatus();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return <code>true</code> if the {@link Target} has not jet provided
|
* @return <code>true</code> if the {@link Target} has not jet provided
|
||||||
* {@link #getControllerAttributes()}.
|
* {@link #getControllerAttributes()}.
|
||||||
|
|||||||
@@ -81,4 +81,10 @@ public interface TargetFilterQuery extends TenantAwareBaseEntity {
|
|||||||
* @return the user that triggered the auto assignment
|
* @return the user that triggered the auto assignment
|
||||||
*/
|
*/
|
||||||
String getAutoAssignInitiatedBy();
|
String getAutoAssignInitiatedBy();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return if confirmation is required for configured auto assignment
|
||||||
|
* (considered with confirmation flow active)
|
||||||
|
*/
|
||||||
|
boolean isConfirmationRequired();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ public class TargetWithActionType {
|
|||||||
private String maintenanceSchedule;
|
private String maintenanceSchedule;
|
||||||
private String maintenanceWindowDuration;
|
private String maintenanceWindowDuration;
|
||||||
private String maintenanceWindowTimeZone;
|
private String maintenanceWindowTimeZone;
|
||||||
|
private final boolean confirmationRequired;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor that uses {@link ActionType#FORCED}
|
* Constructor that uses {@link ActionType#FORCED}
|
||||||
@@ -38,7 +39,7 @@ public class TargetWithActionType {
|
|||||||
* ID if the controller
|
* ID if the controller
|
||||||
*/
|
*/
|
||||||
public TargetWithActionType(final String controllerId) {
|
public TargetWithActionType(final String controllerId) {
|
||||||
this(controllerId, ActionType.FORCED, 0, null);
|
this(controllerId, ActionType.FORCED, 0, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,17 +50,22 @@ public class TargetWithActionType {
|
|||||||
* @param actionType
|
* @param actionType
|
||||||
* specified for the action.
|
* specified for the action.
|
||||||
* @param forceTime
|
* @param forceTime
|
||||||
* after that point in time the action is exposed as forced in
|
* after that point in time the action is exposed as forced in case
|
||||||
* case the type is {@link ActionType#TIMEFORCED}
|
* the type is {@link ActionType#TIMEFORCED}
|
||||||
* @param weight
|
* @param weight
|
||||||
* the priority of an {@link Action}
|
* the priority of an {@link Action}
|
||||||
|
* @param confirmationRequired
|
||||||
|
* sets the confirmation required flag when starting the
|
||||||
|
* {@link Action}
|
||||||
*/
|
*/
|
||||||
public TargetWithActionType(final String controllerId, final ActionType actionType, final long forceTime,
|
public TargetWithActionType(
|
||||||
final Integer weight) {
|
final String controllerId, final ActionType actionType, final long forceTime,
|
||||||
|
final Integer weight, final boolean confirmationRequired) {
|
||||||
this.controllerId = controllerId;
|
this.controllerId = controllerId;
|
||||||
this.actionType = actionType != null ? actionType : ActionType.FORCED;
|
this.actionType = actionType != null ? actionType : ActionType.FORCED;
|
||||||
this.forceTime = forceTime;
|
this.forceTime = forceTime;
|
||||||
this.weight = weight;
|
this.weight = weight;
|
||||||
|
this.confirmationRequired = confirmationRequired;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,8 +99,8 @@ public class TargetWithActionType {
|
|||||||
*/
|
*/
|
||||||
public TargetWithActionType(final String controllerId, final ActionType actionType, final long forceTime,
|
public TargetWithActionType(final String controllerId, final ActionType actionType, final long forceTime,
|
||||||
final Integer weight, final String maintenanceSchedule, final String maintenanceWindowDuration,
|
final Integer weight, final String maintenanceSchedule, final String maintenanceWindowDuration,
|
||||||
final String maintenanceWindowTimeZone) {
|
final String maintenanceWindowTimeZone, final boolean confirmationRequired) {
|
||||||
this(controllerId, actionType, forceTime, weight);
|
this(controllerId, actionType, forceTime, weight, confirmationRequired);
|
||||||
|
|
||||||
this.maintenanceSchedule = maintenanceSchedule;
|
this.maintenanceSchedule = maintenanceSchedule;
|
||||||
this.maintenanceWindowDuration = maintenanceWindowDuration;
|
this.maintenanceWindowDuration = maintenanceWindowDuration;
|
||||||
@@ -152,18 +158,27 @@ public class TargetWithActionType {
|
|||||||
return maintenanceWindowTimeZone;
|
return maintenanceWindowTimeZone;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return if a confirmation is required for this assignment (depends on confirmation flow active)
|
||||||
|
*
|
||||||
|
* @return the flag
|
||||||
|
*/
|
||||||
|
public boolean isConfirmationRequired() {
|
||||||
|
return confirmationRequired;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "TargetWithActionType [controllerId=" + controllerId + ", actionType=" + getActionType() + ", forceTime="
|
return "TargetWithActionType [controllerId=" + controllerId + ", actionType=" + getActionType() + ", forceTime="
|
||||||
+ getForceTime() + ", weight=" + getWeight() + ", maintenanceSchedule=" + getMaintenanceSchedule()
|
+ getForceTime() + ", weight=" + getWeight() + ", maintenanceSchedule=" + getMaintenanceSchedule()
|
||||||
+ ", maintenanceWindowDuration=" + getMaintenanceWindowDuration() + ", maintenanceWindowTimeZone="
|
+ ", maintenanceWindowDuration=" + getMaintenanceWindowDuration() + ", maintenanceWindowTimeZone="
|
||||||
+ getMaintenanceWindowTimeZone() + "]";
|
+ getMaintenanceWindowTimeZone() + ", confirmationRequired=" + isConfirmationRequired() + "]";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash(actionType, controllerId, forceTime, weight, maintenanceSchedule, maintenanceWindowDuration,
|
return Objects.hash(actionType, controllerId, forceTime, weight, confirmationRequired, maintenanceSchedule,
|
||||||
maintenanceWindowTimeZone);
|
maintenanceWindowDuration, maintenanceWindowTimeZone);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("squid:S1067")
|
@SuppressWarnings("squid:S1067")
|
||||||
@@ -181,8 +196,10 @@ public class TargetWithActionType {
|
|||||||
final TargetWithActionType other = (TargetWithActionType) obj;
|
final TargetWithActionType other = (TargetWithActionType) obj;
|
||||||
return Objects.equals(actionType, other.actionType) && Objects.equals(controllerId, other.controllerId)
|
return Objects.equals(actionType, other.actionType) && Objects.equals(controllerId, other.controllerId)
|
||||||
&& Objects.equals(forceTime, other.forceTime) && Objects.equals(weight, other.weight)
|
&& Objects.equals(forceTime, other.forceTime) && Objects.equals(weight, other.weight)
|
||||||
|
&& Objects.equals(confirmationRequired, other.confirmationRequired)
|
||||||
&& Objects.equals(maintenanceSchedule, other.maintenanceSchedule)
|
&& Objects.equals(maintenanceSchedule, other.maintenanceSchedule)
|
||||||
&& Objects.equals(maintenanceWindowDuration, other.maintenanceWindowDuration)
|
&& Objects.equals(maintenanceWindowDuration, other.maintenanceWindowDuration)
|
||||||
&& Objects.equals(maintenanceWindowTimeZone, other.maintenanceWindowTimeZone);
|
&& Objects.equals(maintenanceWindowTimeZone, other.maintenanceWindowTimeZone);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ public class TotalTargetCountStatus {
|
|||||||
case RUNNING:
|
case RUNNING:
|
||||||
case WARNING:
|
case WARNING:
|
||||||
case DOWNLOAD:
|
case DOWNLOAD:
|
||||||
|
case WAIT_FOR_CONFIRMATION:
|
||||||
case CANCELING:
|
case CANCELING:
|
||||||
return Status.RUNNING;
|
return Status.RUNNING;
|
||||||
case DOWNLOADED:
|
case DOWNLOADED:
|
||||||
|
|||||||
@@ -152,6 +152,11 @@ public class TenantConfigurationProperties {
|
|||||||
*/
|
*/
|
||||||
public static final String BATCH_ASSIGNMENTS_ENABLED = "batch.assignments.enabled";
|
public static final String BATCH_ASSIGNMENTS_ENABLED = "batch.assignments.enabled";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch to enable/disable the user-confirmation flow
|
||||||
|
*/
|
||||||
|
public static final String USER_CONFIRMATION_ENABLED = "user.confirmation.flow.enabled";
|
||||||
|
|
||||||
private String keyName;
|
private String keyName;
|
||||||
private String defaultValue = "";
|
private String defaultValue = "";
|
||||||
private Class<?> dataType = String.class;
|
private Class<?> dataType = String.class;
|
||||||
|
|||||||
@@ -6,9 +6,10 @@
|
|||||||
* which accompanies this distribution, and is available at
|
* which accompanies this distribution, and is available at
|
||||||
* http://www.eclipse.org/legal/epl-v10.html
|
* http://www.eclipse.org/legal/epl-v10.html
|
||||||
*/
|
*/
|
||||||
package org.eclipse.hawkbit.repository.jpa.utils;
|
package org.eclipse.hawkbit.utils;
|
||||||
|
|
||||||
import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED;
|
import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED;
|
||||||
|
import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.USER_CONFIRMATION_ENABLED;
|
||||||
|
|
||||||
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
|
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
|
||||||
import org.eclipse.hawkbit.security.SystemSecurityContext;
|
import org.eclipse.hawkbit.security.SystemSecurityContext;
|
||||||
@@ -50,4 +51,14 @@ public final class TenantConfigHelper {
|
|||||||
return systemSecurityContext.runAsSystem(() -> tenantConfigurationManagement
|
return systemSecurityContext.runAsSystem(() -> tenantConfigurationManagement
|
||||||
.getConfigurationValue(MULTI_ASSIGNMENTS_ENABLED, Boolean.class).getValue());
|
.getConfigurationValue(MULTI_ASSIGNMENTS_ENABLED, Boolean.class).getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is confirmation flow enabled for the current tenant
|
||||||
|
*
|
||||||
|
* @return is enabled
|
||||||
|
*/
|
||||||
|
public boolean isConfirmationFlowEnabled() {
|
||||||
|
return systemSecurityContext.runAsSystem(() -> tenantConfigurationManagement
|
||||||
|
.getConfigurationValue(USER_CONFIRMATION_ENABLED, Boolean.class).getValue());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -23,6 +23,7 @@ public abstract class AbstractRolloutGroupCreate<T> extends AbstractNamedEntityB
|
|||||||
protected String targetFilterQuery;
|
protected String targetFilterQuery;
|
||||||
protected Float targetPercentage;
|
protected Float targetPercentage;
|
||||||
protected RolloutGroupConditions conditions;
|
protected RolloutGroupConditions conditions;
|
||||||
|
protected boolean confirmationRequired;
|
||||||
|
|
||||||
public T targetFilterQuery(final String targetFilterQuery) {
|
public T targetFilterQuery(final String targetFilterQuery) {
|
||||||
this.targetFilterQuery = StringUtils.trimWhitespace(targetFilterQuery);
|
this.targetFilterQuery = StringUtils.trimWhitespace(targetFilterQuery);
|
||||||
@@ -39,4 +40,9 @@ public abstract class AbstractRolloutGroupCreate<T> extends AbstractNamedEntityB
|
|||||||
return (T) this;
|
return (T) this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public T confirmationRequired(final boolean confirmationRequired) {
|
||||||
|
this.confirmationRequired = confirmationRequired;
|
||||||
|
return (T) this;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ public abstract class AbstractTargetFilterQueryUpdateCreate<T> extends AbstractB
|
|||||||
@Max(Action.WEIGHT_MAX)
|
@Max(Action.WEIGHT_MAX)
|
||||||
protected Integer weight;
|
protected Integer weight;
|
||||||
|
|
||||||
|
protected Boolean confirmationRequired;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set DS ID of the {@link Action} created during auto assignment
|
* Set DS ID of the {@link Action} created during auto assignment
|
||||||
*
|
*
|
||||||
@@ -120,4 +122,19 @@ public abstract class AbstractTargetFilterQueryUpdateCreate<T> extends AbstractB
|
|||||||
public Optional<String> getQuery() {
|
public Optional<String> getQuery() {
|
||||||
return Optional.ofNullable(query);
|
return Optional.ofNullable(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param confirmationRequired
|
||||||
|
* if confirmation is required for configured auto assignment
|
||||||
|
* (considered with confirmation flow active)
|
||||||
|
* @return updated builder instance
|
||||||
|
*/
|
||||||
|
public T confirmationRequired(final boolean confirmationRequired) {
|
||||||
|
this.confirmationRequired = confirmationRequired;
|
||||||
|
return (T) this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Boolean> getConfirmationRequired() {
|
||||||
|
return Optional.ofNullable(confirmationRequired);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,5 +101,9 @@ hawkbit.server.tenant.configuration.batch-assignments-enabled.defaultValue=false
|
|||||||
hawkbit.server.tenant.configuration.batch-assignments-enabled.dataType=java.lang.Boolean
|
hawkbit.server.tenant.configuration.batch-assignments-enabled.dataType=java.lang.Boolean
|
||||||
hawkbit.server.tenant.configuration.batch-assignments-enabled.validator=org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationBooleanValidator
|
hawkbit.server.tenant.configuration.batch-assignments-enabled.validator=org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationBooleanValidator
|
||||||
|
|
||||||
|
hawkbit.server.tenant.configuration.user-confirmation-enabled.keyName=user.confirmation.flow.enabled
|
||||||
|
hawkbit.server.tenant.configuration.user-confirmation-enabled.defaultValue=false
|
||||||
|
hawkbit.server.tenant.configuration.user-confirmation-enabled.dataType=java.lang.Boolean
|
||||||
|
hawkbit.server.tenant.configuration.user-confirmation-enabled.validator=org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationBooleanValidator
|
||||||
|
|
||||||
# Default tenant configuration - END
|
# Default tenant configuration - END
|
||||||
|
|||||||
@@ -57,11 +57,13 @@ public abstract class AbstractDsAssignmentStrategy {
|
|||||||
private final ActionStatusRepository actionStatusRepository;
|
private final ActionStatusRepository actionStatusRepository;
|
||||||
private final QuotaManagement quotaManagement;
|
private final QuotaManagement quotaManagement;
|
||||||
private final BooleanSupplier multiAssignmentsConfig;
|
private final BooleanSupplier multiAssignmentsConfig;
|
||||||
|
private final BooleanSupplier confirmationFlowConfig;
|
||||||
|
|
||||||
AbstractDsAssignmentStrategy(final TargetRepository targetRepository,
|
AbstractDsAssignmentStrategy(final TargetRepository targetRepository,
|
||||||
final AfterTransactionCommitExecutor afterCommit, final EventPublisherHolder eventPublisherHolder,
|
final AfterTransactionCommitExecutor afterCommit, final EventPublisherHolder eventPublisherHolder,
|
||||||
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
|
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
|
||||||
final QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig) {
|
final QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig,
|
||||||
|
final BooleanSupplier confirmationFlowConfig) {
|
||||||
this.targetRepository = targetRepository;
|
this.targetRepository = targetRepository;
|
||||||
this.afterCommit = afterCommit;
|
this.afterCommit = afterCommit;
|
||||||
this.eventPublisherHolder = eventPublisherHolder;
|
this.eventPublisherHolder = eventPublisherHolder;
|
||||||
@@ -69,6 +71,7 @@ public abstract class AbstractDsAssignmentStrategy {
|
|||||||
this.actionStatusRepository = actionStatusRepository;
|
this.actionStatusRepository = actionStatusRepository;
|
||||||
this.quotaManagement = quotaManagement;
|
this.quotaManagement = quotaManagement;
|
||||||
this.multiAssignmentsConfig = multiAssignmentsConfig;
|
this.multiAssignmentsConfig = multiAssignmentsConfig;
|
||||||
|
this.confirmationFlowConfig = confirmationFlowConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -276,4 +279,8 @@ public abstract class AbstractDsAssignmentStrategy {
|
|||||||
protected boolean isMultiAssignmentsEnabled() {
|
protected boolean isMultiAssignmentsEnabled() {
|
||||||
return multiAssignmentsConfig.getAsBoolean();
|
return multiAssignmentsConfig.getAsBoolean();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected boolean isConfirmationFlowEnabled() {
|
||||||
|
return confirmationFlowConfig.getAsBoolean();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -233,8 +233,8 @@ public interface ActionRepository extends BaseEntityRepository<JpaAction, Long>,
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all {@link Action}s of a specific target and given active flag
|
* Retrieves all {@link Action}s of a specific target and given active flag
|
||||||
* ordered by action ID. Loads also the lazy
|
* ordered by action ID. Loads also the lazy {@link Action#getDistributionSet()}
|
||||||
* {@link Action#getDistributionSet()} field.
|
* field.
|
||||||
*
|
*
|
||||||
* @param pageable
|
* @param pageable
|
||||||
* page parameters
|
* page parameters
|
||||||
@@ -251,9 +251,8 @@ public interface ActionRepository extends BaseEntityRepository<JpaAction, Long>,
|
|||||||
@Param("active") boolean active);
|
@Param("active") boolean active);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Switches the status of actions from one specific status into another,
|
* Switches the status of actions from one specific status into another, only if
|
||||||
* only if the actions are in a specific status. This should be a atomar
|
* the actions are in a specific status. This should be a atomar operation.
|
||||||
* operation.
|
|
||||||
*
|
*
|
||||||
* @param statusToSet
|
* @param statusToSet
|
||||||
* the new status the actions should get
|
* the new status the actions should get
|
||||||
@@ -270,6 +269,21 @@ public interface ActionRepository extends BaseEntityRepository<JpaAction, Long>,
|
|||||||
void switchStatus(@Param("statusToSet") Action.Status statusToSet, @Param("targetsIds") List<Long> targetIds,
|
void switchStatus(@Param("statusToSet") Action.Status statusToSet, @Param("targetsIds") List<Long> targetIds,
|
||||||
@Param("active") boolean active, @Param("currentStatus") Action.Status currentStatus);
|
@Param("active") boolean active, @Param("currentStatus") Action.Status currentStatus);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Retrieves all active {@link Action}s by given controllerId filtered by a
|
||||||
|
* status
|
||||||
|
*
|
||||||
|
* @param controllerId
|
||||||
|
* the IDs of targets for the actions
|
||||||
|
* @param status
|
||||||
|
* the current status of the actions
|
||||||
|
* @return the found list of {@link Action}
|
||||||
|
*/
|
||||||
|
@Query("SELECT a FROM JpaAction a WHERE a.target.controllerId = :controllerId AND a.active = true AND a.status = :status")
|
||||||
|
List<JpaAction> findByTargetIdAndIsActiveAndActionStatus(@Param("controllerId") String controllerId,
|
||||||
|
@Param("status") Action.Status status);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* Retrieves all IDs for {@link Action}s referring to the given target IDs,
|
* Retrieves all IDs for {@link Action}s referring to the given target IDs,
|
||||||
@@ -490,8 +504,8 @@ public interface ActionRepository extends BaseEntityRepository<JpaAction, Long>,
|
|||||||
boolean existsByRolloutIdAndStatusNotIn(@Param("rolloutId") Long rolloutId, @Param("status") Status status);
|
boolean existsByRolloutIdAndStatusNotIn(@Param("rolloutId") Long rolloutId, @Param("status") Status status);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieving all actions referring to a given rollout with a specific
|
* Retrieving all actions referring to a given rollout with a specific action as
|
||||||
* action as parent reference and a specific status.
|
* parent reference and a specific status.
|
||||||
*
|
*
|
||||||
* Finding all actions of a specific rolloutgroup parent relation.
|
* Finding all actions of a specific rolloutgroup parent relation.
|
||||||
*
|
*
|
||||||
@@ -506,7 +520,7 @@ public interface ActionRepository extends BaseEntityRepository<JpaAction, Long>,
|
|||||||
* @return the actions referring a specific rollout and a specific parent
|
* @return the actions referring a specific rollout and a specific parent
|
||||||
* rolloutgroup in a specific status
|
* rolloutgroup in a specific status
|
||||||
*/
|
*/
|
||||||
@EntityGraph(attributePaths = { "target" }, type = EntityGraphType.LOAD)
|
@EntityGraph(attributePaths = { "target", "target.autoConfirmationStatus", "rolloutGroup" }, type = EntityGraphType.LOAD)
|
||||||
Page<Action> findByRolloutIdAndRolloutGroupParentIdAndStatus(Pageable pageable, Long rollout,
|
Page<Action> findByRolloutIdAndRolloutGroupParentIdAndStatus(Pageable pageable, Long rollout,
|
||||||
Long rolloutGroupParent, Status actionStatus);
|
Long rolloutGroupParent, Status actionStatus);
|
||||||
|
|
||||||
@@ -522,7 +536,8 @@ public interface ActionRepository extends BaseEntityRepository<JpaAction, Long>,
|
|||||||
* @return the actions referring a specific rollout and a specific parent
|
* @return the actions referring a specific rollout and a specific parent
|
||||||
* rolloutgroup in a specific status
|
* rolloutgroup in a specific status
|
||||||
*/
|
*/
|
||||||
@EntityGraph(attributePaths = { "target" }, type = EntityGraphType.LOAD)
|
@EntityGraph(attributePaths = { "target", "target.autoConfirmationStatus",
|
||||||
|
"rolloutGroup" }, type = EntityGraphType.LOAD)
|
||||||
Page<Action> findByRolloutIdAndRolloutGroupParentIsNullAndStatus(Pageable pageable, Long rollout,
|
Page<Action> findByRolloutIdAndRolloutGroupParentIsNullAndStatus(Pageable pageable, Long rollout,
|
||||||
Status actionStatus);
|
Status actionStatus);
|
||||||
|
|
||||||
|
|||||||
@@ -14,21 +14,41 @@ import java.util.Comparator;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.eclipse.hawkbit.repository.QuotaManagement;
|
||||||
import org.eclipse.hawkbit.repository.RepositoryProperties;
|
import org.eclipse.hawkbit.repository.RepositoryProperties;
|
||||||
|
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
||||||
|
import org.eclipse.hawkbit.repository.jpa.builder.JpaActionStatusCreate;
|
||||||
|
import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
|
||||||
|
import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus;
|
||||||
|
import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper;
|
||||||
import org.eclipse.hawkbit.repository.model.Action;
|
import org.eclipse.hawkbit.repository.model.Action;
|
||||||
|
import org.eclipse.hawkbit.repository.model.ActionStatus;
|
||||||
|
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
|
||||||
|
import static org.eclipse.hawkbit.repository.model.Action.ActionType.DOWNLOAD_ONLY;
|
||||||
|
import static org.eclipse.hawkbit.repository.model.Action.Status.FINISHED;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements utility methods for managing {@link Action}s
|
* Implements utility methods for managing {@link Action}s
|
||||||
*/
|
*/
|
||||||
public class JpaActionManagement {
|
public class JpaActionManagement {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(JpaActionManagement.class);
|
||||||
|
|
||||||
protected final ActionRepository actionRepository;
|
protected final ActionRepository actionRepository;
|
||||||
|
protected final ActionStatusRepository actionStatusRepository;
|
||||||
|
protected final QuotaManagement quotaManagement;
|
||||||
protected final RepositoryProperties repositoryProperties;
|
protected final RepositoryProperties repositoryProperties;
|
||||||
|
|
||||||
protected JpaActionManagement(final ActionRepository actionRepository,
|
protected JpaActionManagement(final ActionRepository actionRepository,
|
||||||
|
final ActionStatusRepository actionStatusRepository, final QuotaManagement quotaManagement,
|
||||||
final RepositoryProperties repositoryProperties) {
|
final RepositoryProperties repositoryProperties) {
|
||||||
this.actionRepository = actionRepository;
|
this.actionRepository = actionRepository;
|
||||||
|
this.actionStatusRepository = actionStatusRepository;
|
||||||
|
this.quotaManagement = quotaManagement;
|
||||||
this.repositoryProperties = repositoryProperties;
|
this.repositoryProperties = repositoryProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,8 +70,90 @@ public class JpaActionManagement {
|
|||||||
return actions.stream().sorted(actionImportance).limit(maxActionCount).collect(Collectors.toList());
|
return actions.stream().sorted(actionImportance).limit(maxActionCount).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected List<JpaAction> findActiveActionsHavingStatus(final String controllerId, final Action.Status status) {
|
||||||
|
if (!actionRepository.activeActionExistsForControllerId(controllerId)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return Collections
|
||||||
|
.unmodifiableList(actionRepository.findByTargetIdAndIsActiveAndActionStatus(controllerId, status));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Action addActionStatus(final JpaActionStatusCreate statusCreate) {
|
||||||
|
final Long actionId = statusCreate.getActionId();
|
||||||
|
final JpaActionStatus actionStatus = statusCreate.build();
|
||||||
|
final JpaAction action = getActionAndThrowExceptionIfNotFound(actionId);
|
||||||
|
|
||||||
|
if (isUpdatingActionStatusAllowed(action, actionStatus)) {
|
||||||
|
return handleAddUpdateActionStatus(actionStatus, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.debug("Update of actionStatus {} for action {} not possible since action not active anymore.",
|
||||||
|
actionStatus.getStatus(), action.getId());
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ActionStatus updates are allowed mainly if the action is active. If the
|
||||||
|
* action is not active we accept further status updates if permitted so by
|
||||||
|
* repository configuration. In this case, only the values: Status.ERROR and
|
||||||
|
* Status.FINISHED are allowed. In the case of a DOWNLOAD_ONLY action, we accept
|
||||||
|
* status updates only once.
|
||||||
|
*/
|
||||||
|
protected boolean isUpdatingActionStatusAllowed(final JpaAction action, final JpaActionStatus actionStatus) {
|
||||||
|
|
||||||
|
final boolean isIntermediateFeedback = (FINISHED != actionStatus.getStatus())
|
||||||
|
&& (Action.Status.ERROR != actionStatus.getStatus());
|
||||||
|
|
||||||
|
final boolean isAllowedByRepositoryConfiguration = !repositoryProperties.isRejectActionStatusForClosedAction()
|
||||||
|
&& isIntermediateFeedback;
|
||||||
|
|
||||||
|
final boolean isAllowedForDownloadOnlyActions = isDownloadOnly(action) && !isIntermediateFeedback;
|
||||||
|
|
||||||
|
return action.isActive() || isAllowedByRepositoryConfiguration || isAllowedForDownloadOnlyActions;
|
||||||
|
}
|
||||||
|
|
||||||
protected int getWeightConsideringDefault(final Action action) {
|
protected int getWeightConsideringDefault(final Action action) {
|
||||||
return action.getWeight().orElse(repositoryProperties.getActionWeightIfAbsent());
|
return action.getWeight().orElse(repositoryProperties.getActionWeightIfAbsent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected JpaAction getActionAndThrowExceptionIfNotFound(final Long actionId) {
|
||||||
|
return actionRepository.findById(actionId)
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException(Action.class, actionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static boolean isDownloadOnly(final JpaAction action) {
|
||||||
|
return DOWNLOAD_ONLY == action.getActionType();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets {@link TargetUpdateStatus} based on given {@link ActionStatus}.
|
||||||
|
*/
|
||||||
|
protected Action handleAddUpdateActionStatus(final JpaActionStatus actionStatus, final JpaAction action) {
|
||||||
|
// information status entry - check for a potential DOS attack
|
||||||
|
assertActionStatusQuota(action);
|
||||||
|
assertActionStatusMessageQuota(actionStatus);
|
||||||
|
actionStatus.setAction(action);
|
||||||
|
|
||||||
|
onActionStatusUpdate(actionStatus.getStatus(), action);
|
||||||
|
|
||||||
|
actionStatusRepository.save(actionStatus);
|
||||||
|
|
||||||
|
action.setLastActionStatusCode(actionStatus.getCode().orElse(null));
|
||||||
|
return actionRepository.save(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onActionStatusUpdate(final Action.Status updatedActionStatus, final JpaAction action){
|
||||||
|
// can be overwritten to intercept the persistence of the action status
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void assertActionStatusQuota(final JpaAction action) {
|
||||||
|
QuotaHelper.assertAssignmentQuota(action.getId(), 1, quotaManagement.getMaxStatusEntriesPerAction(),
|
||||||
|
ActionStatus.class, Action.class, actionStatusRepository::countByActionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void assertActionStatusMessageQuota(final JpaActionStatus actionStatus) {
|
||||||
|
QuotaHelper.assertAssignmentQuota(actionStatus.getId(), actionStatus.getMessages().size(),
|
||||||
|
quotaManagement.getMaxMessagesPerActionStatus(), "Message", ActionStatus.class.getSimpleName(), null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2022 Bosch.IO 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 java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.eclipse.hawkbit.repository.ConfirmationManagement;
|
||||||
|
import org.eclipse.hawkbit.repository.EntityFactory;
|
||||||
|
import org.eclipse.hawkbit.repository.QuotaManagement;
|
||||||
|
import org.eclipse.hawkbit.repository.RepositoryConstants;
|
||||||
|
import org.eclipse.hawkbit.repository.RepositoryProperties;
|
||||||
|
import org.eclipse.hawkbit.repository.builder.ActionStatusCreate;
|
||||||
|
import org.eclipse.hawkbit.repository.exception.AutoConfirmationAlreadyActiveException;
|
||||||
|
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
||||||
|
import org.eclipse.hawkbit.repository.exception.InvalidConfirmationFeedbackException;
|
||||||
|
import org.eclipse.hawkbit.repository.jpa.builder.JpaActionStatusCreate;
|
||||||
|
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
|
||||||
|
import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
|
||||||
|
import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus;
|
||||||
|
import org.eclipse.hawkbit.repository.jpa.model.JpaAutoConfirmationStatus;
|
||||||
|
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
|
||||||
|
import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications;
|
||||||
|
import org.eclipse.hawkbit.repository.model.Action;
|
||||||
|
import org.eclipse.hawkbit.repository.model.Action.Status;
|
||||||
|
import org.eclipse.hawkbit.repository.model.AutoConfirmationStatus;
|
||||||
|
import org.eclipse.hawkbit.repository.model.Target;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.dao.ConcurrencyFailureException;
|
||||||
|
import org.springframework.retry.annotation.Backoff;
|
||||||
|
import org.springframework.retry.annotation.Retryable;
|
||||||
|
import org.springframework.transaction.annotation.Isolation;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JPA implementation for {@link ConfirmationManagement}.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Validated
|
||||||
|
public class JpaConfirmationManagement extends JpaActionManagement implements ConfirmationManagement {
|
||||||
|
|
||||||
|
public static final String CONFIRMATION_CODE_MSG_PREFIX = "Confirmation status code: %d";
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(JpaConfirmationManagement.class);
|
||||||
|
|
||||||
|
private final EntityFactory entityFactory;
|
||||||
|
private final TargetRepository targetRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
protected JpaConfirmationManagement(final TargetRepository targetRepository,
|
||||||
|
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
|
||||||
|
final RepositoryProperties repositoryProperties, final QuotaManagement quotaManagement,
|
||||||
|
final EntityFactory entityFactory) {
|
||||||
|
super(actionRepository, actionStatusRepository, quotaManagement, repositoryProperties);
|
||||||
|
this.targetRepository = targetRepository;
|
||||||
|
this.entityFactory = entityFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Action> findActiveActionsWaitingConfirmation(final String controllerId) {
|
||||||
|
return Collections.unmodifiableList(findActiveActionsHavingStatus(controllerId, Status.WAIT_FOR_CONFIRMATION));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public AutoConfirmationStatus activateAutoConfirmation(final String controllerId, final String initiator,
|
||||||
|
final String remark) {
|
||||||
|
LOG.trace("'activateAutoConfirmation' was called with values: controllerId={}; initiator={}; remark={}",
|
||||||
|
controllerId, initiator, remark);
|
||||||
|
final JpaTarget target = getTargetByControllerIdAndThrowIfNotFound(controllerId);
|
||||||
|
if (target.getAutoConfirmationStatus() != null) {
|
||||||
|
LOG.debug("'activateAutoConfirmation' was called for an controller id {} with active auto confirmation.",
|
||||||
|
controllerId);
|
||||||
|
throw new AutoConfirmationAlreadyActiveException(controllerId);
|
||||||
|
}
|
||||||
|
final JpaAutoConfirmationStatus confirmationStatus = new JpaAutoConfirmationStatus(initiator, remark, target);
|
||||||
|
target.setAutoConfirmationStatus(confirmationStatus);
|
||||||
|
final JpaTarget updatedTarget = targetRepository.save(target);
|
||||||
|
final AutoConfirmationStatus autoConfStatus = updatedTarget.getAutoConfirmationStatus();
|
||||||
|
if (autoConfStatus == null) {
|
||||||
|
final String message = String.format("Persisted auto confirmation status is null. "
|
||||||
|
+ "Cannot proceed with giving confirmations for active actions for device %s with initiator %s.",
|
||||||
|
controllerId, initiator);
|
||||||
|
LOG.error("message");
|
||||||
|
throw new IllegalStateException(message);
|
||||||
|
}
|
||||||
|
giveConfirmationForActiveActions(autoConfStatus);
|
||||||
|
return autoConfStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<AutoConfirmationStatus> getStatus(final String controllerId) {
|
||||||
|
return Optional.of(getTargetByControllerIdAndThrowIfNotFound(controllerId)).map(JpaTarget::getAutoConfirmationStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public List<Action> autoConfirmActiveActions(final String controllerId) {
|
||||||
|
final JpaTarget target = getTargetByControllerIdAndThrowIfNotFound(controllerId);
|
||||||
|
if (target.getAutoConfirmationStatus() == null) {
|
||||||
|
// auto-confirmation is not active
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return giveConfirmationForActiveActions(target.getAutoConfirmationStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(isolation = Isolation.READ_COMMITTED)
|
||||||
|
@Retryable(include = {
|
||||||
|
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
|
||||||
|
public Action confirmAction(final long actionId, final Integer code, final Collection<String> deviceMessages) {
|
||||||
|
LOG.trace("Action with id {} confirm request is triggered.", actionId);
|
||||||
|
final Action action = getActionAndThrowExceptionIfNotFound(actionId);
|
||||||
|
assertActionCanAcceptFeedback(action);
|
||||||
|
final List<String> messages = new ArrayList<>();
|
||||||
|
if (deviceMessages != null) {
|
||||||
|
messages.addAll(deviceMessages);
|
||||||
|
}
|
||||||
|
messages.add(RepositoryConstants.SERVER_MESSAGE_PREFIX + "Target confirmed action."
|
||||||
|
+ " Therefore, it will be set to the running state to proceed with the deployment.");
|
||||||
|
final ActionStatusCreate statusCreate = createConfirmationActionStatus(action.getId(), code, messages)
|
||||||
|
.status(Status.RUNNING);
|
||||||
|
return addActionStatus((JpaActionStatusCreate) statusCreate);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(isolation = Isolation.READ_COMMITTED)
|
||||||
|
@Retryable(include = {
|
||||||
|
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
|
||||||
|
public Action denyAction(final long actionId, final Integer code, final Collection<String> deviceMessages) {
|
||||||
|
LOG.trace("Action with id {} deny request is triggered.", actionId);
|
||||||
|
final Action action = getActionAndThrowExceptionIfNotFound(actionId);
|
||||||
|
assertActionCanAcceptFeedback(action);
|
||||||
|
final List<String> messages = new ArrayList<>();
|
||||||
|
if (deviceMessages != null) {
|
||||||
|
messages.addAll(deviceMessages);
|
||||||
|
}
|
||||||
|
messages.add(RepositoryConstants.SERVER_MESSAGE_PREFIX + "Target rejected action."
|
||||||
|
+ " Action will stay in confirmation pending state.");
|
||||||
|
final ActionStatusCreate statusCreate = createConfirmationActionStatus(action.getId(), code, messages)
|
||||||
|
.status(Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
return addActionStatus((JpaActionStatusCreate) statusCreate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ActionStatusCreate createConfirmationActionStatus(final long actionId, final Integer code,
|
||||||
|
final Collection<String> messages) {
|
||||||
|
final ActionStatusCreate statusCreate = entityFactory.actionStatus().create(actionId);
|
||||||
|
if (!CollectionUtils.isEmpty(messages)) {
|
||||||
|
statusCreate.messages(messages);
|
||||||
|
}
|
||||||
|
if (code != null) {
|
||||||
|
statusCreate.code(code);
|
||||||
|
statusCreate.message(String.format(CONFIRMATION_CODE_MSG_PREFIX, code));
|
||||||
|
}
|
||||||
|
return statusCreate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertActionCanAcceptFeedback(final Action action) {
|
||||||
|
if (!action.isActive()) {
|
||||||
|
final String msg = String.format(
|
||||||
|
"Confirming action %s is not possible since the action is not active anymore.", action.getId());
|
||||||
|
LOG.warn(msg);
|
||||||
|
throw new InvalidConfirmationFeedbackException(InvalidConfirmationFeedbackException.Reason.ACTION_CLOSED,
|
||||||
|
msg);
|
||||||
|
} else if (!action.isWaitingConfirmation()) {
|
||||||
|
LOG.debug("Action is not waiting for confirmation, deny request.");
|
||||||
|
final String msg = String.format("Action %s is not waiting for confirmation.", action.getId());
|
||||||
|
LOG.warn(msg);
|
||||||
|
throw new InvalidConfirmationFeedbackException(
|
||||||
|
InvalidConfirmationFeedbackException.Reason.NOT_AWAITING_CONFIRMATION, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Action> giveConfirmationForActiveActions(final AutoConfirmationStatus autoConfirmationStatus) {
|
||||||
|
final Target target = autoConfirmationStatus.getTarget();
|
||||||
|
return findActiveActionsHavingStatus(target.getControllerId(), Status.WAIT_FOR_CONFIRMATION).stream()
|
||||||
|
.map(action -> autoConfirmAction(action, autoConfirmationStatus)).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Action autoConfirmAction(final JpaAction action, final AutoConfirmationStatus autoConfirmationStatus) {
|
||||||
|
if (!action.isWaitingConfirmation()) {
|
||||||
|
LOG.debug("Auto-confirming action is not necessary, since action {} is in RUNNING state already.",
|
||||||
|
action.getId());
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
final JpaActionStatus actionStatus = (JpaActionStatus) entityFactory.actionStatus().create(action.getId())
|
||||||
|
.status(Status.RUNNING)
|
||||||
|
.messages(Collections.singletonList(autoConfirmationStatus.constructActionMessage())).build();
|
||||||
|
LOG.debug(
|
||||||
|
"Automatically confirm actionId '{}' due to active auto-confirmation initiated by '{}' and rollouts system user '{}'",
|
||||||
|
action.getId(), autoConfirmationStatus.getInitiator(), autoConfirmationStatus.getCreatedBy());
|
||||||
|
|
||||||
|
// do not make use of
|
||||||
|
// org.eclipse.hawkbit.repository.jpa.JpaActionManagement.handleAddUpdateActionStatus
|
||||||
|
// to bypass the quota check. Otherwise the action will not be confirmed in case
|
||||||
|
// of exceeded action status quota.
|
||||||
|
action.setStatus(Status.RUNNING);
|
||||||
|
actionStatus.setAction(action);
|
||||||
|
|
||||||
|
actionStatusRepository.save(actionStatus);
|
||||||
|
return actionRepository.save(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void deactivateAutoConfirmation(String controllerId) {
|
||||||
|
LOG.debug("Deactivate auto confirmation for controllerId '{}'", controllerId);
|
||||||
|
final JpaTarget target = getTargetByControllerIdAndThrowIfNotFound(controllerId);
|
||||||
|
target.setAutoConfirmationStatus(null);
|
||||||
|
targetRepository.save(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JpaTarget getTargetByControllerIdAndThrowIfNotFound(final String controllerId) {
|
||||||
|
return targetRepository.findOne(TargetSpecifications.hasControllerId(controllerId))
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onActionStatusUpdate(final Status updatedActionStatus, final JpaAction action) {
|
||||||
|
if (updatedActionStatus == Status.RUNNING && action.isActive()) {
|
||||||
|
action.setStatus(Status.RUNNING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@
|
|||||||
*/
|
*/
|
||||||
package org.eclipse.hawkbit.repository.jpa;
|
package org.eclipse.hawkbit.repository.jpa;
|
||||||
|
|
||||||
import static org.eclipse.hawkbit.repository.model.Action.ActionType.DOWNLOAD_ONLY;
|
|
||||||
import static org.eclipse.hawkbit.repository.model.Action.Status.DOWNLOADED;
|
import static org.eclipse.hawkbit.repository.model.Action.Status.DOWNLOADED;
|
||||||
import static org.eclipse.hawkbit.repository.model.Action.Status.FINISHED;
|
import static org.eclipse.hawkbit.repository.model.Action.Status.FINISHED;
|
||||||
import static org.eclipse.hawkbit.repository.model.Target.CONTROLLER_ATTRIBUTE_KEY_SIZE;
|
import static org.eclipse.hawkbit.repository.model.Target.CONTROLLER_ATTRIBUTE_KEY_SIZE;
|
||||||
@@ -39,6 +38,7 @@ import javax.persistence.criteria.Root;
|
|||||||
import javax.validation.constraints.NotEmpty;
|
import javax.validation.constraints.NotEmpty;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import org.eclipse.hawkbit.repository.ConfirmationManagement;
|
||||||
import org.eclipse.hawkbit.repository.ControllerManagement;
|
import org.eclipse.hawkbit.repository.ControllerManagement;
|
||||||
import org.eclipse.hawkbit.repository.EntityFactory;
|
import org.eclipse.hawkbit.repository.EntityFactory;
|
||||||
import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper;
|
import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper;
|
||||||
@@ -72,6 +72,7 @@ import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper;
|
|||||||
import org.eclipse.hawkbit.repository.model.Action;
|
import org.eclipse.hawkbit.repository.model.Action;
|
||||||
import org.eclipse.hawkbit.repository.model.Action.Status;
|
import org.eclipse.hawkbit.repository.model.Action.Status;
|
||||||
import org.eclipse.hawkbit.repository.model.ActionStatus;
|
import org.eclipse.hawkbit.repository.model.ActionStatus;
|
||||||
|
import org.eclipse.hawkbit.repository.model.AutoConfirmationStatus;
|
||||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||||
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
||||||
import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata;
|
import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata;
|
||||||
@@ -125,12 +126,6 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
|
|||||||
@Autowired
|
@Autowired
|
||||||
private SoftwareModuleRepository softwareModuleRepository;
|
private SoftwareModuleRepository softwareModuleRepository;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private ActionStatusRepository actionStatusRepository;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private QuotaManagement quotaManagement;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private TenantConfigurationManagement tenantConfigurationManagement;
|
private TenantConfigurationManagement tenantConfigurationManagement;
|
||||||
|
|
||||||
@@ -155,9 +150,13 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
|
|||||||
@Autowired
|
@Autowired
|
||||||
private TenantAware tenantAware;
|
private TenantAware tenantAware;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ConfirmationManagement confirmationManagement;
|
||||||
|
|
||||||
public JpaControllerManagement(final ScheduledExecutorService executorService,
|
public JpaControllerManagement(final ScheduledExecutorService executorService,
|
||||||
final RepositoryProperties repositoryProperties, final ActionRepository actionRepository) {
|
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
|
||||||
super(actionRepository, repositoryProperties);
|
final QuotaManagement quotaManagement, final RepositoryProperties repositoryProperties) {
|
||||||
|
super(actionRepository, actionStatusRepository, quotaManagement, repositoryProperties);
|
||||||
|
|
||||||
if (!repositoryProperties.isEagerPollPersistence()) {
|
if (!repositoryProperties.isEagerPollPersistence()) {
|
||||||
executorService.scheduleWithFixedDelay(this::flushUpdateQueue,
|
executorService.scheduleWithFixedDelay(this::flushUpdateQueue,
|
||||||
@@ -565,11 +564,6 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
|
|||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertActionStatusMessageQuota(final JpaActionStatus actionStatus) {
|
|
||||||
QuotaHelper.assertAssignmentQuota(actionStatus.getId(), actionStatus.getMessages().size(),
|
|
||||||
quotaManagement.getMaxMessagesPerActionStatus(), "Message", ActionStatus.class.getSimpleName(), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleFinishedCancelation(final JpaActionStatus actionStatus, final JpaAction action) {
|
private void handleFinishedCancelation(final JpaActionStatus actionStatus, final JpaAction action) {
|
||||||
// in case of successful cancellation we also report the success at
|
// in case of successful cancellation we also report the success at
|
||||||
// the canceled action itself.
|
// the canceled action itself.
|
||||||
@@ -582,88 +576,41 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
|
|||||||
@Transactional(isolation = Isolation.READ_COMMITTED)
|
@Transactional(isolation = Isolation.READ_COMMITTED)
|
||||||
@Retryable(include = {
|
@Retryable(include = {
|
||||||
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
|
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
|
||||||
public Action addUpdateActionStatus(final ActionStatusCreate c) {
|
public Action addUpdateActionStatus(final ActionStatusCreate statusCreate) {
|
||||||
final JpaActionStatusCreate create = (JpaActionStatusCreate) c;
|
return addActionStatus((JpaActionStatusCreate) statusCreate);
|
||||||
final JpaAction action = getActionAndThrowExceptionIfNotFound(create.getActionId());
|
|
||||||
final JpaActionStatus actionStatus = create.build();
|
|
||||||
|
|
||||||
if (isUpdatingActionStatusAllowed(action, actionStatus)) {
|
|
||||||
return handleAddUpdateActionStatus(actionStatus, action);
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG.debug("Update of actionStatus {} for action {} not possible since action not active anymore.",
|
|
||||||
actionStatus.getStatus(), action.getId());
|
|
||||||
return action;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* ActionStatus updates are allowed mainly if the action is active. If the
|
protected void onActionStatusUpdate(final Action.Status updatedActionStatus, final JpaAction action) {
|
||||||
* action is not active we accept further status updates if permitted so by
|
switch (updatedActionStatus) {
|
||||||
* repository configuration. In this case, only the values: Status.ERROR and
|
|
||||||
* Status.FINISHED are allowed. In the case of a DOWNLOAD_ONLY action, we accept
|
|
||||||
* status updates only once.
|
|
||||||
*/
|
|
||||||
private boolean isUpdatingActionStatusAllowed(final JpaAction action, final JpaActionStatus actionStatus) {
|
|
||||||
|
|
||||||
final boolean isIntermediateFeedback = (FINISHED != actionStatus.getStatus())
|
|
||||||
&& (Status.ERROR != actionStatus.getStatus());
|
|
||||||
|
|
||||||
final boolean isAllowedByRepositoryConfiguration = !repositoryProperties.isRejectActionStatusForClosedAction()
|
|
||||||
&& isIntermediateFeedback;
|
|
||||||
|
|
||||||
final boolean isAllowedForDownloadOnlyActions = isDownloadOnly(action) && !isIntermediateFeedback;
|
|
||||||
|
|
||||||
return action.isActive() || isAllowedByRepositoryConfiguration || isAllowedForDownloadOnlyActions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isDownloadOnly(final JpaAction action) {
|
|
||||||
return DOWNLOAD_ONLY == action.getActionType();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets {@link TargetUpdateStatus} based on given {@link ActionStatus}.
|
|
||||||
*/
|
|
||||||
private Action handleAddUpdateActionStatus(final JpaActionStatus actionStatus, final JpaAction action) {
|
|
||||||
|
|
||||||
String controllerId = null;
|
|
||||||
LOG.debug("handleAddUpdateActionStatus for action {}", action.getId());
|
|
||||||
|
|
||||||
// information status entry - check for a potential DOS attack
|
|
||||||
assertActionStatusQuota(action);
|
|
||||||
assertActionStatusMessageQuota(actionStatus);
|
|
||||||
|
|
||||||
switch (actionStatus.getStatus()) {
|
|
||||||
case ERROR:
|
case ERROR:
|
||||||
final JpaTarget target = (JpaTarget) action.getTarget();
|
final JpaTarget target = (JpaTarget) action.getTarget();
|
||||||
target.setUpdateStatus(TargetUpdateStatus.ERROR);
|
target.setUpdateStatus(TargetUpdateStatus.ERROR);
|
||||||
handleErrorOnAction(action, target);
|
handleErrorOnAction(action, target);
|
||||||
break;
|
break;
|
||||||
case FINISHED:
|
case FINISHED:
|
||||||
controllerId = handleFinishedAndStoreInTargetStatus(action);
|
handleFinishedAndStoreInTargetStatus(action).ifPresent(this::requestControllerAttributes);
|
||||||
break;
|
break;
|
||||||
case DOWNLOADED:
|
case DOWNLOADED:
|
||||||
controllerId = handleDownloadedActionStatus(action);
|
handleDownloadedActionStatus(action).ifPresent(this::requestControllerAttributes);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
actionStatus.setAction(action);
|
|
||||||
actionStatusRepository.save(actionStatus);
|
|
||||||
|
|
||||||
action.setLastActionStatusCode(actionStatus.getCode().orElse(null));
|
|
||||||
final Action savedAction = actionRepository.save(action);
|
|
||||||
|
|
||||||
if (controllerId != null) {
|
|
||||||
requestControllerAttributes(controllerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return savedAction;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String handleDownloadedActionStatus(final JpaAction action) {
|
/**
|
||||||
|
* Handles the case where the {@link Action.Status#DOWNLOADED} status is
|
||||||
|
* reported by the device. In case the update is finished, a controllerId will
|
||||||
|
* be returned to trigger a request for attributes.
|
||||||
|
*
|
||||||
|
* @param action
|
||||||
|
* updated action
|
||||||
|
* @return a present controllerId in case the attributes needs to be requested.
|
||||||
|
*/
|
||||||
|
private Optional<String> handleDownloadedActionStatus(final JpaAction action) {
|
||||||
if (!isDownloadOnly(action)) {
|
if (!isDownloadOnly(action)) {
|
||||||
return null;
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
final JpaTarget target = (JpaTarget) action.getTarget();
|
final JpaTarget target = (JpaTarget) action.getTarget();
|
||||||
@@ -672,7 +619,7 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
|
|||||||
target.setUpdateStatus(TargetUpdateStatus.IN_SYNC);
|
target.setUpdateStatus(TargetUpdateStatus.IN_SYNC);
|
||||||
targetRepository.save(target);
|
targetRepository.save(target);
|
||||||
|
|
||||||
return target.getControllerId();
|
return Optional.of(target.getControllerId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requestControllerAttributes(final String controllerId) {
|
private void requestControllerAttributes(final String controllerId) {
|
||||||
@@ -695,12 +642,16 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
|
|||||||
targetRepository.save(mergedTarget);
|
targetRepository.save(mergedTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertActionStatusQuota(final JpaAction action) {
|
/**
|
||||||
QuotaHelper.assertAssignmentQuota(action.getId(), 1, quotaManagement.getMaxStatusEntriesPerAction(),
|
* Handles the case where the {@link Action.Status#FINISHED} status is
|
||||||
ActionStatus.class, Action.class, actionStatusRepository::countByActionId);
|
* reported by the device. In case the update is finished, a controllerId will
|
||||||
}
|
* be returned to trigger a request for attributes.
|
||||||
|
*
|
||||||
private String handleFinishedAndStoreInTargetStatus(final JpaAction action) {
|
* @param action
|
||||||
|
* updated action
|
||||||
|
* @return a present controllerId in case the attributes needs to be requested.
|
||||||
|
*/
|
||||||
|
private Optional<String> handleFinishedAndStoreInTargetStatus(final JpaAction action) {
|
||||||
final JpaTarget target = (JpaTarget) action.getTarget();
|
final JpaTarget target = (JpaTarget) action.getTarget();
|
||||||
action.setActive(false);
|
action.setActive(false);
|
||||||
action.setStatus(Status.FINISHED);
|
action.setStatus(Status.FINISHED);
|
||||||
@@ -727,7 +678,7 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
|
|||||||
targetRepository.save(target);
|
targetRepository.save(target);
|
||||||
entityManager.detach(ds);
|
entityManager.detach(ds);
|
||||||
|
|
||||||
return target.getControllerId();
|
return Optional.of(target.getControllerId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -884,11 +835,6 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
|
|||||||
return actionStatusRepository.save(statusMessage);
|
return actionStatusRepository.save(statusMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private JpaAction getActionAndThrowExceptionIfNotFound(final Long actionId) {
|
|
||||||
return actionRepository.findById(actionId)
|
|
||||||
.orElseThrow(() -> new EntityNotFoundException(Action.class, actionId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<Target> getByControllerId(final String controllerId) {
|
public Optional<Target> getByControllerId(final String controllerId) {
|
||||||
return targetRepository.findOne(TargetSpecifications.hasControllerId(controllerId)).map(Target.class::cast);
|
return targetRepository.findOne(TargetSpecifications.hasControllerId(controllerId)).map(Target.class::cast);
|
||||||
@@ -1076,6 +1022,17 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AutoConfirmationStatus activateAutoConfirmation(final String controllerId, final String initiator,
|
||||||
|
final String remark) {
|
||||||
|
return confirmationManagement.activateAutoConfirmation(controllerId, initiator, remark);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deactivateAutoConfirmation(final String controllerId) {
|
||||||
|
confirmationManagement.deactivateAutoConfirmation(controllerId);
|
||||||
|
}
|
||||||
|
|
||||||
private void cancelAssignDistributionSetEvent(final Action action) {
|
private void cancelAssignDistributionSetEvent(final Action action) {
|
||||||
afterCommit.afterCommit(() -> eventPublisherHolder.getEventPublisher().publishEvent(
|
afterCommit.afterCommit(() -> eventPublisherHolder.getEventPublisher().publishEvent(
|
||||||
new CancelTargetAssignmentEvent(action, eventPublisherHolder.getApplicationId())));
|
new CancelTargetAssignmentEvent(action, eventPublisherHolder.getApplicationId())));
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import java.util.Arrays;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.EnumMap;
|
import java.util.EnumMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
@@ -61,7 +62,6 @@ import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility;
|
|||||||
import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications;
|
import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications;
|
||||||
import org.eclipse.hawkbit.repository.jpa.utils.DeploymentHelper;
|
import org.eclipse.hawkbit.repository.jpa.utils.DeploymentHelper;
|
||||||
import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper;
|
import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper;
|
||||||
import org.eclipse.hawkbit.repository.jpa.utils.TenantConfigHelper;
|
|
||||||
import org.eclipse.hawkbit.repository.jpa.utils.WeightValidationHelper;
|
import org.eclipse.hawkbit.repository.jpa.utils.WeightValidationHelper;
|
||||||
import org.eclipse.hawkbit.repository.model.Action;
|
import org.eclipse.hawkbit.repository.model.Action;
|
||||||
import org.eclipse.hawkbit.repository.model.Action.ActionType;
|
import org.eclipse.hawkbit.repository.model.Action.ActionType;
|
||||||
@@ -81,6 +81,7 @@ 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.security.SystemSecurityContext;
|
import org.eclipse.hawkbit.security.SystemSecurityContext;
|
||||||
import org.eclipse.hawkbit.tenancy.TenantAware;
|
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||||
|
import org.eclipse.hawkbit.utils.TenantConfigHelper;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.dao.ConcurrencyFailureException;
|
import org.springframework.dao.ConcurrencyFailureException;
|
||||||
@@ -138,14 +139,12 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
|
|||||||
private final DistributionSetManagement distributionSetManagement;
|
private final DistributionSetManagement distributionSetManagement;
|
||||||
private final DistributionSetRepository distributionSetRepository;
|
private final DistributionSetRepository distributionSetRepository;
|
||||||
private final TargetRepository targetRepository;
|
private final TargetRepository targetRepository;
|
||||||
private final ActionStatusRepository actionStatusRepository;
|
|
||||||
private final AuditorAware<String> auditorProvider;
|
private final AuditorAware<String> auditorProvider;
|
||||||
private final VirtualPropertyReplacer virtualPropertyReplacer;
|
private final VirtualPropertyReplacer virtualPropertyReplacer;
|
||||||
private final PlatformTransactionManager txManager;
|
private final PlatformTransactionManager txManager;
|
||||||
private final OnlineDsAssignmentStrategy onlineDsAssignmentStrategy;
|
private final OnlineDsAssignmentStrategy onlineDsAssignmentStrategy;
|
||||||
private final OfflineDsAssignmentStrategy offlineDsAssignmentStrategy;
|
private final OfflineDsAssignmentStrategy offlineDsAssignmentStrategy;
|
||||||
private final TenantConfigurationManagement tenantConfigurationManagement;
|
private final TenantConfigurationManagement tenantConfigurationManagement;
|
||||||
private final QuotaManagement quotaManagement;
|
|
||||||
private final SystemSecurityContext systemSecurityContext;
|
private final SystemSecurityContext systemSecurityContext;
|
||||||
private final TenantAware tenantAware;
|
private final TenantAware tenantAware;
|
||||||
private final Database database;
|
private final Database database;
|
||||||
@@ -160,26 +159,25 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
|
|||||||
final TenantConfigurationManagement tenantConfigurationManagement, final QuotaManagement quotaManagement,
|
final TenantConfigurationManagement tenantConfigurationManagement, final QuotaManagement quotaManagement,
|
||||||
final SystemSecurityContext systemSecurityContext, final TenantAware tenantAware, final Database database,
|
final SystemSecurityContext systemSecurityContext, final TenantAware tenantAware, final Database database,
|
||||||
final RepositoryProperties repositoryProperties) {
|
final RepositoryProperties repositoryProperties) {
|
||||||
super(actionRepository, repositoryProperties);
|
super(actionRepository, actionStatusRepository, quotaManagement, repositoryProperties);
|
||||||
this.entityManager = entityManager;
|
this.entityManager = entityManager;
|
||||||
this.distributionSetRepository = distributionSetRepository;
|
this.distributionSetRepository = distributionSetRepository;
|
||||||
this.distributionSetManagement = distributionSetManagement;
|
this.distributionSetManagement = distributionSetManagement;
|
||||||
this.targetRepository = targetRepository;
|
this.targetRepository = targetRepository;
|
||||||
this.actionStatusRepository = actionStatusRepository;
|
|
||||||
this.auditorProvider = auditorProvider;
|
this.auditorProvider = auditorProvider;
|
||||||
this.virtualPropertyReplacer = virtualPropertyReplacer;
|
this.virtualPropertyReplacer = virtualPropertyReplacer;
|
||||||
this.txManager = txManager;
|
this.txManager = txManager;
|
||||||
onlineDsAssignmentStrategy = new OnlineDsAssignmentStrategy(targetRepository, afterCommit, eventPublisherHolder,
|
onlineDsAssignmentStrategy = new OnlineDsAssignmentStrategy(targetRepository, afterCommit, eventPublisherHolder,
|
||||||
actionRepository, actionStatusRepository, quotaManagement, this::isMultiAssignmentsEnabled);
|
actionRepository, actionStatusRepository, quotaManagement, this::isMultiAssignmentsEnabled,
|
||||||
|
this::isConfirmationFlowEnabled);
|
||||||
offlineDsAssignmentStrategy = new OfflineDsAssignmentStrategy(targetRepository, afterCommit,
|
offlineDsAssignmentStrategy = new OfflineDsAssignmentStrategy(targetRepository, afterCommit,
|
||||||
eventPublisherHolder, actionRepository, actionStatusRepository, quotaManagement,
|
eventPublisherHolder, actionRepository, actionStatusRepository, quotaManagement,
|
||||||
this::isMultiAssignmentsEnabled);
|
this::isMultiAssignmentsEnabled, this::isConfirmationFlowEnabled);
|
||||||
this.tenantConfigurationManagement = tenantConfigurationManagement;
|
this.tenantConfigurationManagement = tenantConfigurationManagement;
|
||||||
this.quotaManagement = quotaManagement;
|
|
||||||
this.systemSecurityContext = systemSecurityContext;
|
this.systemSecurityContext = systemSecurityContext;
|
||||||
this.tenantAware = tenantAware;
|
this.tenantAware = tenantAware;
|
||||||
this.database = database;
|
this.database = database;
|
||||||
retryTemplate = createRetryTemplate();
|
this.retryTemplate = createRetryTemplate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -393,7 +391,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
|
|||||||
targetEntitiesIdsChunks.forEach(this::cancelInactiveScheduledActionsForTargets);
|
targetEntitiesIdsChunks.forEach(this::cancelInactiveScheduledActionsForTargets);
|
||||||
setAssignedDistributionSetAndTargetUpdateStatus(assignmentStrategy, distributionSetEntity,
|
setAssignedDistributionSetAndTargetUpdateStatus(assignmentStrategy, distributionSetEntity,
|
||||||
targetEntitiesIdsChunks);
|
targetEntitiesIdsChunks);
|
||||||
final List<JpaAction> assignedActions = createActions(initiatedBy, targetsWithActionType, targetEntities,
|
final Map<TargetWithActionType, JpaAction> assignedActions = createActions(initiatedBy, targetsWithActionType, targetEntities,
|
||||||
assignmentStrategy, distributionSetEntity);
|
assignmentStrategy, distributionSetEntity);
|
||||||
// create initial action status when action is created so we remember
|
// create initial action status when action is created so we remember
|
||||||
// the initial running status because we will change the status
|
// the initial running status because we will change the status
|
||||||
@@ -402,7 +400,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
|
|||||||
createActionsStatus(assignedActions, assignmentStrategy, actionMessage);
|
createActionsStatus(assignedActions, assignmentStrategy, actionMessage);
|
||||||
|
|
||||||
detachEntitiesAndSendTargetUpdatedEvents(distributionSetEntity, targetEntities, assignmentStrategy);
|
detachEntitiesAndSendTargetUpdatedEvents(distributionSetEntity, targetEntities, assignmentStrategy);
|
||||||
return assignedActions;
|
return new ArrayList<>(assignedActions.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -473,20 +471,62 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
|
|||||||
assignmentStrategy.setAssignedDistributionSetAndTargetStatus(set, targetIdsChunks, currentUser);
|
assignmentStrategy.setAssignedDistributionSetAndTargetStatus(set, targetIdsChunks, currentUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<JpaAction> createActions(final String initiatedBy,
|
private Map<TargetWithActionType, JpaAction> createActions(final String initiatedBy,
|
||||||
final Collection<TargetWithActionType> targetsWithActionType, final List<JpaTarget> targets,
|
final Collection<TargetWithActionType> targetsWithActionType, final List<JpaTarget> targets,
|
||||||
final AbstractDsAssignmentStrategy assignmentStrategy, final JpaDistributionSet set) {
|
final AbstractDsAssignmentStrategy assignmentStrategy, final JpaDistributionSet set) {
|
||||||
|
|
||||||
return targetsWithActionType.stream()
|
final Map<TargetWithActionType, JpaAction> persistedActions = new LinkedHashMap<>();
|
||||||
.map(twt -> assignmentStrategy.createTargetAction(initiatedBy, twt, targets, set))
|
|
||||||
.filter(Objects::nonNull).map(actionRepository::save).collect(Collectors.toList());
|
for (final TargetWithActionType twt : targetsWithActionType) {
|
||||||
|
final JpaAction targetAction = assignmentStrategy.createTargetAction(initiatedBy, twt, targets, set);
|
||||||
|
if (targetAction != null) {
|
||||||
|
persistedActions.put(twt, actionRepository.save(targetAction));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return persistedActions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createActionsStatus(final Collection<JpaAction> actions,
|
private void createActionsStatus(final Map<TargetWithActionType, JpaAction> actions,
|
||||||
final AbstractDsAssignmentStrategy assignmentStrategy, final String actionMessage) {
|
final AbstractDsAssignmentStrategy assignmentStrategy, final String actionMessage) {
|
||||||
actionStatusRepository
|
actionStatusRepository.saveAll(actions.entrySet().stream().map(entry -> {
|
||||||
.saveAll(actions.stream().map(action -> assignmentStrategy.createActionStatus(action, actionMessage))
|
final JpaAction action = entry.getValue();
|
||||||
.collect(Collectors.toList()));
|
final JpaActionStatus actionStatus = assignmentStrategy.createActionStatus(action, actionMessage);
|
||||||
|
verifyAndAddConfirmationStatus(action, actionStatus, entry.getKey().isConfirmationRequired());
|
||||||
|
return actionStatus;
|
||||||
|
}).collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setInitialActionStatusOfRolloutGroup(final List<JpaAction> actions) {
|
||||||
|
final List<JpaActionStatus> statusList = new ArrayList<>();
|
||||||
|
for (final JpaAction action : actions) {
|
||||||
|
final JpaActionStatus actionStatus = onlineDsAssignmentStrategy.createActionStatus(action, null);
|
||||||
|
verifyAndAddConfirmationStatus(action, actionStatus, action.getRolloutGroup().isConfirmationRequired());
|
||||||
|
statusList.add(actionStatus);
|
||||||
|
}
|
||||||
|
actionStatusRepository.saveAll(statusList);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyAndAddConfirmationStatus(final JpaAction action, final JpaActionStatus actionStatus,
|
||||||
|
final boolean isConfirmationRequired) {
|
||||||
|
if (actionStatus.getStatus() == Status.WAIT_FOR_CONFIRMATION) {
|
||||||
|
if (action.getStatus().equals(Status.RUNNING)) {
|
||||||
|
// action is in RUNNING state only if it's confirmed during assignment already
|
||||||
|
if (!isConfirmationRequired) {
|
||||||
|
// confirmation given on assignment dialog
|
||||||
|
actionStatus.addMessage(
|
||||||
|
String.format("Assignment confirmed by initiator [%s].", action.getInitiatedBy()));
|
||||||
|
} else if (action.getTarget().getAutoConfirmationStatus() != null) {
|
||||||
|
// auto-confirmation is configured
|
||||||
|
actionStatus.addMessage(action.getTarget().getAutoConfirmationStatus().constructActionMessage());
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Action in RUNNING state without given confirmation.");
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
actionStatus
|
||||||
|
.addMessage("Waiting for the confirmation by the device before processing with the deployment");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void detachEntitiesAndSendTargetUpdatedEvents(final JpaDistributionSet set, final List<JpaTarget> targets,
|
private void detachEntitiesAndSendTargetUpdatedEvents(final JpaDistributionSet set, final List<JpaTarget> targets,
|
||||||
@@ -643,15 +683,15 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
|
|||||||
if (!isMultiAssignmentsEnabled()) {
|
if (!isMultiAssignmentsEnabled()) {
|
||||||
closeOrCancelOpenDeviceActions(actions);
|
closeOrCancelOpenDeviceActions(actions);
|
||||||
}
|
}
|
||||||
final List<JpaAction> savedActions = activateActions(actions);
|
final List<JpaAction> savedActions = activateActionsOfRolloutGroup(actions);
|
||||||
setInitialActionStatus(savedActions);
|
setInitialActionStatusOfRolloutGroup(savedActions);
|
||||||
setAssignmentOnTargets(savedActions);
|
setAssignmentOnTargets(savedActions);
|
||||||
return Collections.unmodifiableList(savedActions);
|
return Collections.unmodifiableList(savedActions);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void closeOrCancelOpenDeviceActions(final List<JpaAction> actions) {
|
private void closeOrCancelOpenDeviceActions(final List<JpaAction> actions){
|
||||||
final List<Long> targetIds = actions.stream().map(JpaAction::getTarget).map(Target::getId)
|
final List<Long> targetIds = actions.stream().map(JpaAction::getTarget).map(Target::getId)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
if (isActionsAutocloseEnabled()) {
|
if (isActionsAutocloseEnabled()) {
|
||||||
onlineDsAssignmentStrategy.closeObsoleteUpdateActions(targetIds);
|
onlineDsAssignmentStrategy.closeObsoleteUpdateActions(targetIds);
|
||||||
} else {
|
} else {
|
||||||
@@ -659,9 +699,15 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<JpaAction> activateActions(final List<JpaAction> actions) {
|
private List<JpaAction> activateActionsOfRolloutGroup(final List<JpaAction> actions) {
|
||||||
actions.forEach(action -> {
|
actions.forEach(action -> {
|
||||||
action.setActive(true);
|
action.setActive(true);
|
||||||
|
final boolean confirmationRequired = action.getRolloutGroup().isConfirmationRequired()
|
||||||
|
&& action.getTarget().getAutoConfirmationStatus() == null;
|
||||||
|
if (isConfirmationFlowEnabled() && confirmationRequired) {
|
||||||
|
action.setStatus(Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
return;
|
||||||
|
}
|
||||||
action.setStatus(Status.RUNNING);
|
action.setStatus(Status.RUNNING);
|
||||||
});
|
});
|
||||||
return actionRepository.saveAll(actions);
|
return actionRepository.saveAll(actions);
|
||||||
@@ -678,14 +724,6 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
|
|||||||
targetRepository.saveAll(assignedDsTargets);
|
targetRepository.saveAll(assignedDsTargets);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setInitialActionStatus(final List<JpaAction> actions) {
|
|
||||||
final List<JpaActionStatus> statusList = new ArrayList<>();
|
|
||||||
for (final JpaAction action : actions) {
|
|
||||||
statusList.add(onlineDsAssignmentStrategy.createActionStatus(action, null));
|
|
||||||
}
|
|
||||||
actionStatusRepository.saveAll(statusList);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setSkipActionStatus(final JpaAction action) {
|
private void setSkipActionStatus(final JpaAction action) {
|
||||||
final JpaActionStatus actionStatus = new JpaActionStatus();
|
final JpaActionStatus actionStatus = new JpaActionStatus();
|
||||||
actionStatus.setAction(action);
|
actionStatus.setAction(action);
|
||||||
@@ -745,7 +783,6 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
|
|||||||
public List<Action> findActiveActionsWithHighestWeight(final String controllerId, final int maxActionCount) {
|
public List<Action> findActiveActionsWithHighestWeight(final String controllerId, final int maxActionCount) {
|
||||||
return findActiveActionsWithHighestWeightConsideringDefault(controllerId, maxActionCount);
|
return findActiveActionsWithHighestWeightConsideringDefault(controllerId, maxActionCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getWeightConsideringDefault(final Action action) {
|
public int getWeightConsideringDefault(final Action action) {
|
||||||
return super.getWeightConsideringDefault(action);
|
return super.getWeightConsideringDefault(action);
|
||||||
@@ -967,6 +1004,11 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
|
|||||||
.isMultiAssignmentsEnabled();
|
.isMultiAssignmentsEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isConfirmationFlowEnabled() {
|
||||||
|
return TenantConfigHelper.usingContext(systemSecurityContext, tenantConfigurationManagement)
|
||||||
|
.isConfirmationFlowEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
private <T extends Serializable> T getConfigValue(final String key, final Class<T> valueType) {
|
private <T extends Serializable> T getConfigValue(final String key, final Class<T> valueType) {
|
||||||
return systemSecurityContext
|
return systemSecurityContext
|
||||||
.runAsSystem(() -> tenantConfigurationManagement.getConfigurationValue(key, valueType).getValue());
|
.runAsSystem(() -> tenantConfigurationManagement.getConfigurationValue(key, valueType).getValue());
|
||||||
@@ -1002,5 +1044,4 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ 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.security.SystemSecurityContext;
|
import org.eclipse.hawkbit.security.SystemSecurityContext;
|
||||||
import org.eclipse.hawkbit.tenancy.TenantAware;
|
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||||
|
import org.eclipse.hawkbit.utils.TenantConfigHelper;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -193,10 +194,11 @@ public class JpaRolloutManagement implements RolloutManagement {
|
|||||||
@Transactional
|
@Transactional
|
||||||
@Retryable(include = {
|
@Retryable(include = {
|
||||||
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
|
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
|
||||||
public Rollout create(final RolloutCreate rollout, final int amountGroup, final RolloutGroupConditions conditions) {
|
public Rollout create(final RolloutCreate rollout, final int amountGroup, final boolean confirmationRequired,
|
||||||
|
final RolloutGroupConditions conditions) {
|
||||||
RolloutHelper.verifyRolloutGroupParameter(amountGroup, quotaManagement);
|
RolloutHelper.verifyRolloutGroupParameter(amountGroup, quotaManagement);
|
||||||
final JpaRollout savedRollout = createRollout((JpaRollout) rollout.build());
|
final JpaRollout savedRollout = createRollout((JpaRollout) rollout.build());
|
||||||
return createRolloutGroups(amountGroup, conditions, savedRollout);
|
return createRolloutGroups(amountGroup, conditions, savedRollout, confirmationRequired);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -222,7 +224,7 @@ public class JpaRolloutManagement implements RolloutManagement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Rollout createRolloutGroups(final int amountOfGroups, final RolloutGroupConditions conditions,
|
private Rollout createRolloutGroups(final int amountOfGroups, final RolloutGroupConditions conditions,
|
||||||
final JpaRollout rollout) {
|
final JpaRollout rollout, final boolean isConfirmationRequired) {
|
||||||
RolloutHelper.verifyRolloutInStatus(rollout, RolloutStatus.CREATING);
|
RolloutHelper.verifyRolloutInStatus(rollout, RolloutStatus.CREATING);
|
||||||
RolloutHelper.verifyRolloutGroupConditions(conditions);
|
RolloutHelper.verifyRolloutGroupConditions(conditions);
|
||||||
|
|
||||||
@@ -241,6 +243,7 @@ public class JpaRolloutManagement implements RolloutManagement {
|
|||||||
group.setRollout(savedRollout);
|
group.setRollout(savedRollout);
|
||||||
group.setParent(lastSavedGroup);
|
group.setParent(lastSavedGroup);
|
||||||
group.setStatus(RolloutGroupStatus.CREATING);
|
group.setStatus(RolloutGroupStatus.CREATING);
|
||||||
|
group.setConfirmationRequired(isConfirmationRequired);
|
||||||
|
|
||||||
addSuccessAndErrorConditionsAndActions(group, conditions);
|
addSuccessAndErrorConditionsAndActions(group, conditions);
|
||||||
|
|
||||||
@@ -283,6 +286,7 @@ public class JpaRolloutManagement implements RolloutManagement {
|
|||||||
group.setRollout(savedRollout);
|
group.setRollout(savedRollout);
|
||||||
group.setParent(lastSavedGroup);
|
group.setParent(lastSavedGroup);
|
||||||
group.setStatus(RolloutGroupStatus.CREATING);
|
group.setStatus(RolloutGroupStatus.CREATING);
|
||||||
|
group.setConfirmationRequired(srcGroup.isConfirmationRequired());
|
||||||
|
|
||||||
group.setTargetPercentage(srcGroup.getTargetPercentage());
|
group.setTargetPercentage(srcGroup.getTargetPercentage());
|
||||||
if (srcGroup.getTargetFilterQuery() != null) {
|
if (srcGroup.getTargetFilterQuery() != null) {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
|
|||||||
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
|
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
|
||||||
import org.eclipse.hawkbit.security.SystemSecurityContext;
|
import org.eclipse.hawkbit.security.SystemSecurityContext;
|
||||||
import org.eclipse.hawkbit.tenancy.TenantAware;
|
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||||
|
import org.eclipse.hawkbit.utils.TenantConfigHelper;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.dao.ConcurrencyFailureException;
|
import org.springframework.dao.ConcurrencyFailureException;
|
||||||
@@ -249,6 +250,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme
|
|||||||
// set the new query
|
// set the new query
|
||||||
targetFilterQuery.setQuery(query);
|
targetFilterQuery.setQuery(query);
|
||||||
});
|
});
|
||||||
|
update.getConfirmationRequired().ifPresent(targetFilterQuery::setConfirmationRequired);
|
||||||
|
|
||||||
return targetFilterQueryRepository.save(targetFilterQuery);
|
return targetFilterQueryRepository.save(targetFilterQuery);
|
||||||
}
|
}
|
||||||
@@ -263,6 +265,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme
|
|||||||
targetFilterQuery.setAutoAssignActionType(null);
|
targetFilterQuery.setAutoAssignActionType(null);
|
||||||
targetFilterQuery.setAutoAssignWeight(null);
|
targetFilterQuery.setAutoAssignWeight(null);
|
||||||
targetFilterQuery.setAutoAssignInitiatedBy(null);
|
targetFilterQuery.setAutoAssignInitiatedBy(null);
|
||||||
|
targetFilterQuery.setConfirmationRequired(false);
|
||||||
} else {
|
} else {
|
||||||
WeightValidationHelper.usingContext(systemSecurityContext, tenantConfigurationManagement).validate(update);
|
WeightValidationHelper.usingContext(systemSecurityContext, tenantConfigurationManagement).validate(update);
|
||||||
// we cannot be sure that the quota was enforced at creation time
|
// we cannot be sure that the quota was enforced at creation time
|
||||||
@@ -277,10 +280,18 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme
|
|||||||
targetFilterQuery.setAutoAssignInitiatedBy(tenantAware.getCurrentUsername());
|
targetFilterQuery.setAutoAssignInitiatedBy(tenantAware.getCurrentUsername());
|
||||||
targetFilterQuery.setAutoAssignActionType(sanitizeAutoAssignActionType(update.getActionType()));
|
targetFilterQuery.setAutoAssignActionType(sanitizeAutoAssignActionType(update.getActionType()));
|
||||||
targetFilterQuery.setAutoAssignWeight(update.getWeight());
|
targetFilterQuery.setAutoAssignWeight(update.getWeight());
|
||||||
|
final boolean confirmationRequired = update.isConfirmationRequired() == null ? isConfirmationFlowEnabled()
|
||||||
|
: update.isConfirmationRequired();
|
||||||
|
targetFilterQuery.setConfirmationRequired(confirmationRequired);
|
||||||
}
|
}
|
||||||
return targetFilterQueryRepository.save(targetFilterQuery);
|
return targetFilterQueryRepository.save(targetFilterQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isConfirmationFlowEnabled() {
|
||||||
|
return TenantConfigHelper.usingContext(systemSecurityContext, tenantConfigurationManagement)
|
||||||
|
.isConfirmationFlowEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
private static void verifyDistributionSetAndThrowExceptionIfDeleted(final DistributionSet distributionSet) {
|
private static void verifyDistributionSetAndThrowExceptionIfDeleted(final DistributionSet distributionSet) {
|
||||||
if (distributionSet.isDeleted()) {
|
if (distributionSet.isDeleted()) {
|
||||||
throw new EntityNotFoundException(DistributionSet.class, distributionSet.getId());
|
throw new EntityNotFoundException(DistributionSet.class, distributionSet.getId());
|
||||||
|
|||||||
@@ -40,12 +40,14 @@ import org.eclipse.hawkbit.repository.builder.TargetUpdate;
|
|||||||
import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent;
|
import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent;
|
||||||
import org.eclipse.hawkbit.repository.event.remote.TargetDeletedEvent;
|
import org.eclipse.hawkbit.repository.event.remote.TargetDeletedEvent;
|
||||||
import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
|
import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
|
||||||
|
import org.eclipse.hawkbit.repository.exception.AutoConfirmationAlreadyActiveException;
|
||||||
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
|
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
|
||||||
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
||||||
import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetCreate;
|
import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetCreate;
|
||||||
import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetUpdate;
|
import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetUpdate;
|
||||||
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
|
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
|
||||||
import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor;
|
import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor;
|
||||||
|
import org.eclipse.hawkbit.repository.jpa.model.JpaAutoConfirmationStatus;
|
||||||
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
|
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
|
||||||
import org.eclipse.hawkbit.repository.jpa.model.JpaTargetMetadata;
|
import org.eclipse.hawkbit.repository.jpa.model.JpaTargetMetadata;
|
||||||
import org.eclipse.hawkbit.repository.jpa.model.JpaTargetMetadata_;
|
import org.eclipse.hawkbit.repository.jpa.model.JpaTargetMetadata_;
|
||||||
@@ -57,6 +59,7 @@ import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility;
|
|||||||
import org.eclipse.hawkbit.repository.jpa.specifications.SpecificationsBuilder;
|
import org.eclipse.hawkbit.repository.jpa.specifications.SpecificationsBuilder;
|
||||||
import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications;
|
import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications;
|
||||||
import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper;
|
import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper;
|
||||||
|
import org.eclipse.hawkbit.repository.model.AutoConfirmationStatus;
|
||||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||||
import org.eclipse.hawkbit.repository.model.DistributionSetType;
|
import org.eclipse.hawkbit.repository.model.DistributionSetType;
|
||||||
import org.eclipse.hawkbit.repository.model.MetaData;
|
import org.eclipse.hawkbit.repository.model.MetaData;
|
||||||
|
|||||||
@@ -45,9 +45,10 @@ public class OfflineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
|
|||||||
OfflineDsAssignmentStrategy(final TargetRepository targetRepository,
|
OfflineDsAssignmentStrategy(final TargetRepository targetRepository,
|
||||||
final AfterTransactionCommitExecutor afterCommit, final EventPublisherHolder eventPublisherHolder,
|
final AfterTransactionCommitExecutor afterCommit, final EventPublisherHolder eventPublisherHolder,
|
||||||
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
|
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
|
||||||
final QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig) {
|
final QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig,
|
||||||
|
final BooleanSupplier confirmationFlowConfig) {
|
||||||
super(targetRepository, afterCommit, eventPublisherHolder, actionRepository, actionStatusRepository,
|
super(targetRepository, afterCommit, eventPublisherHolder, actionRepository, actionStatusRepository,
|
||||||
quotaManagement, multiAssignmentsConfig);
|
quotaManagement, multiAssignmentsConfig, confirmationFlowConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -48,9 +48,10 @@ public class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
|
|||||||
OnlineDsAssignmentStrategy(final TargetRepository targetRepository,
|
OnlineDsAssignmentStrategy(final TargetRepository targetRepository,
|
||||||
final AfterTransactionCommitExecutor afterCommit, final EventPublisherHolder eventPublisherHolder,
|
final AfterTransactionCommitExecutor afterCommit, final EventPublisherHolder eventPublisherHolder,
|
||||||
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
|
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
|
||||||
final QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig) {
|
final QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig,
|
||||||
|
final BooleanSupplier confirmationFlowConfig) {
|
||||||
super(targetRepository, afterCommit, eventPublisherHolder, actionRepository, actionStatusRepository,
|
super(targetRepository, afterCommit, eventPublisherHolder, actionRepository, actionStatusRepository,
|
||||||
quotaManagement, multiAssignmentsConfig);
|
quotaManagement, multiAssignmentsConfig, confirmationFlowConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -130,15 +131,28 @@ public class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
|
|||||||
final List<JpaTarget> targets, final JpaDistributionSet set) {
|
final List<JpaTarget> targets, final JpaDistributionSet set) {
|
||||||
final JpaAction result = super.createTargetAction(initiatedBy, targetWithActionType, targets, set);
|
final JpaAction result = super.createTargetAction(initiatedBy, targetWithActionType, targets, set);
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
result.setStatus(Status.RUNNING);
|
final boolean confirmationRequired = targetWithActionType.isConfirmationRequired()
|
||||||
|
&& result.getTarget().getAutoConfirmationStatus() == null;
|
||||||
|
if (isConfirmationFlowEnabled() && confirmationRequired) {
|
||||||
|
result.setStatus(Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
} else {
|
||||||
|
result.setStatus(Status.RUNNING);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will be called to create the initial action status for an action
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public JpaActionStatus createActionStatus(final JpaAction action, final String actionMessage) {
|
public JpaActionStatus createActionStatus(final JpaAction action, final String actionMessage) {
|
||||||
final JpaActionStatus result = super.createActionStatus(action, actionMessage);
|
final JpaActionStatus result = super.createActionStatus(action, actionMessage);
|
||||||
result.setStatus(Status.RUNNING);
|
if (isConfirmationFlowEnabled()) {
|
||||||
|
result.setStatus(Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
} else {
|
||||||
|
result.setStatus(Status.RUNNING);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import org.eclipse.hawkbit.repository.ArtifactEncryptionSecretsStore;
|
|||||||
import org.eclipse.hawkbit.repository.ArtifactEncryptionService;
|
import org.eclipse.hawkbit.repository.ArtifactEncryptionService;
|
||||||
import org.eclipse.hawkbit.repository.ArtifactManagement;
|
import org.eclipse.hawkbit.repository.ArtifactManagement;
|
||||||
import org.eclipse.hawkbit.repository.BaseRepositoryTypeProvider;
|
import org.eclipse.hawkbit.repository.BaseRepositoryTypeProvider;
|
||||||
|
import org.eclipse.hawkbit.repository.ConfirmationManagement;
|
||||||
import org.eclipse.hawkbit.repository.ControllerManagement;
|
import org.eclipse.hawkbit.repository.ControllerManagement;
|
||||||
import org.eclipse.hawkbit.repository.DeploymentManagement;
|
import org.eclipse.hawkbit.repository.DeploymentManagement;
|
||||||
import org.eclipse.hawkbit.repository.DistributionSetManagement;
|
import org.eclipse.hawkbit.repository.DistributionSetManagement;
|
||||||
@@ -742,6 +743,16 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
|
|||||||
quotaManagement, systemSecurityContext, tenantAware, properties.getDatabase(), repositoryProperties);
|
quotaManagement, systemSecurityContext, tenantAware, properties.getDatabase(), repositoryProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
ConfirmationManagement confirmationManagement(final TargetRepository targetRepository,
|
||||||
|
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
|
||||||
|
final RepositoryProperties repositoryProperties, final QuotaManagement quotaManagement,
|
||||||
|
final EntityFactory entityFactory) {
|
||||||
|
return new JpaConfirmationManagement(targetRepository, actionRepository, actionStatusRepository,
|
||||||
|
repositoryProperties, quotaManagement, entityFactory);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link JpaControllerManagement} bean.
|
* {@link JpaControllerManagement} bean.
|
||||||
*
|
*
|
||||||
@@ -750,8 +761,10 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
|
|||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
ControllerManagement controllerManagement(final ScheduledExecutorService executorService,
|
ControllerManagement controllerManagement(final ScheduledExecutorService executorService,
|
||||||
final RepositoryProperties repositoryProperties, final ActionRepository actionRepository) {
|
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
|
||||||
return new JpaControllerManagement(executorService, repositoryProperties, actionRepository);
|
final QuotaManagement quotaManagement, final RepositoryProperties repositoryProperties) {
|
||||||
|
return new JpaControllerManagement(executorService, actionRepository, actionStatusRepository, quotaManagement,
|
||||||
|
repositoryProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ public abstract class AbstractAutoAssignExecutor implements AutoAssignExecutor {
|
|||||||
.map(controllerId -> DeploymentManagement
|
.map(controllerId -> DeploymentManagement
|
||||||
.deploymentRequest(controllerId, filterQuery.getAutoAssignDistributionSet().getId())
|
.deploymentRequest(controllerId, filterQuery.getAutoAssignDistributionSet().getId())
|
||||||
.setActionType(autoAssignActionType).setWeight(filterQuery.getAutoAssignWeight().orElse(null))
|
.setActionType(autoAssignActionType).setWeight(filterQuery.getAutoAssignWeight().orElse(null))
|
||||||
.build())
|
.setConfirmationRequired(filterQuery.isConfirmationRequired()).build())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ public class JpaRolloutGroupCreate extends AbstractRolloutGroupCreate<RolloutGro
|
|||||||
if (conditions != null) {
|
if (conditions != null) {
|
||||||
addSuccessAndErrorConditionsAndActions(group, conditions);
|
addSuccessAndErrorConditionsAndActions(group, conditions);
|
||||||
}
|
}
|
||||||
|
group.setConfirmationRequired(confirmationRequired);
|
||||||
|
|
||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,11 +31,10 @@ public class JpaTargetFilterQueryCreate extends AbstractTargetFilterQueryUpdateC
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JpaTargetFilterQuery build() {
|
public JpaTargetFilterQuery build() {
|
||||||
|
|
||||||
return new JpaTargetFilterQuery(name, query,
|
return new JpaTargetFilterQuery(name, query,
|
||||||
getAutoAssignDistributionSetId().map(distributionSetManagement::getValidAndComplete).orElse(null),
|
getAutoAssignDistributionSetId().map(distributionSetManagement::getValidAndComplete).orElse(null),
|
||||||
getAutoAssignActionType().filter(JpaTargetFilterQueryCreate::isAutoAssignActionTypeValid).orElse(null),
|
getAutoAssignActionType().filter(JpaTargetFilterQueryCreate::isAutoAssignActionTypeValid).orElse(null),
|
||||||
weight);
|
weight, getConfirmationRequired().orElse(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isAutoAssignActionTypeValid(final ActionType actionType) {
|
private static boolean isAutoAssignActionTypeValid(final ActionType actionType) {
|
||||||
|
|||||||
@@ -108,7 +108,8 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio
|
|||||||
@ConversionValue(objectValue = "DOWNLOAD", dataValue = "7"),
|
@ConversionValue(objectValue = "DOWNLOAD", dataValue = "7"),
|
||||||
@ConversionValue(objectValue = "SCHEDULED", dataValue = "8"),
|
@ConversionValue(objectValue = "SCHEDULED", dataValue = "8"),
|
||||||
@ConversionValue(objectValue = "CANCEL_REJECTED", dataValue = "9"),
|
@ConversionValue(objectValue = "CANCEL_REJECTED", dataValue = "9"),
|
||||||
@ConversionValue(objectValue = "DOWNLOADED", dataValue = "10") })
|
@ConversionValue(objectValue = "DOWNLOADED", dataValue = "10"),
|
||||||
|
@ConversionValue(objectValue = "WAIT_FOR_CONFIRMATION", dataValue = "11")})
|
||||||
@Convert("status")
|
@Convert("status")
|
||||||
@NotNull
|
@NotNull
|
||||||
private Status status;
|
private Status status;
|
||||||
@@ -236,7 +237,8 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio
|
|||||||
public String toString() {
|
public String toString() {
|
||||||
return "JpaAction [distributionSet=" + distributionSet.getId() + ", version=" + getOptLockRevision() + ", id="
|
return "JpaAction [distributionSet=" + distributionSet.getId() + ", version=" + getOptLockRevision() + ", id="
|
||||||
+ getId() + ", actionType=" + getActionType() + ", weight=" + getWeight() + ", isActive=" + isActive()
|
+ getId() + ", actionType=" + getActionType() + ", weight=" + getWeight() + ", isActive=" + isActive()
|
||||||
+ ", createdAt=" + getCreatedAt() + ", lastModifiedAt=" + getLastModifiedAt() + "]";
|
+ ", createdAt=" + getCreatedAt() + ", lastModifiedAt=" + getLastModifiedAt() + ", status="
|
||||||
|
+ getStatus().name() + "]";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -387,4 +389,8 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio
|
|||||||
public void setLastActionStatusCode(final Integer lastActionStatusCode) {
|
public void setLastActionStatusCode(final Integer lastActionStatusCode) {
|
||||||
this.lastActionStatusCode = lastActionStatusCode;
|
this.lastActionStatusCode = lastActionStatusCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isWaitingConfirmation() {
|
||||||
|
return status == Status.WAIT_FOR_CONFIRMATION;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,8 @@ public class JpaActionStatus extends AbstractJpaTenantAwareBaseEntity implements
|
|||||||
@ConversionValue(objectValue = "DOWNLOAD", dataValue = "7"),
|
@ConversionValue(objectValue = "DOWNLOAD", dataValue = "7"),
|
||||||
@ConversionValue(objectValue = "SCHEDULED", dataValue = "8"),
|
@ConversionValue(objectValue = "SCHEDULED", dataValue = "8"),
|
||||||
@ConversionValue(objectValue = "CANCEL_REJECTED", dataValue = "9"),
|
@ConversionValue(objectValue = "CANCEL_REJECTED", dataValue = "9"),
|
||||||
@ConversionValue(objectValue = "DOWNLOADED", dataValue = "10")})
|
@ConversionValue(objectValue = "DOWNLOADED", dataValue = "10"),
|
||||||
|
@ConversionValue(objectValue = "WAIT_FOR_CONFIRMATION", dataValue = "11")})
|
||||||
@Convert("status")
|
@Convert("status")
|
||||||
@NotNull
|
@NotNull
|
||||||
private Status status;
|
private Status status;
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2022 Bosch.IO 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.model;
|
||||||
|
|
||||||
|
import org.eclipse.hawkbit.repository.model.AutoConfirmationStatus;
|
||||||
|
import org.eclipse.hawkbit.repository.model.NamedEntity;
|
||||||
|
import org.eclipse.hawkbit.repository.model.Target;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.ConstraintMode;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.FetchType;
|
||||||
|
import javax.persistence.ForeignKey;
|
||||||
|
import javax.persistence.JoinColumn;
|
||||||
|
import javax.persistence.OneToOne;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
import javax.validation.constraints.Size;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "sp_target_conf_status")
|
||||||
|
public class JpaAutoConfirmationStatus extends AbstractJpaTenantAwareBaseEntity implements AutoConfirmationStatus {
|
||||||
|
|
||||||
|
@OneToOne(optional = false, fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "target_id", nullable = false, foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_target_auto_conf"))
|
||||||
|
private JpaTarget target;
|
||||||
|
|
||||||
|
@Column(name = "initiator", length = USERNAME_FIELD_LENGTH)
|
||||||
|
@Size(max = USERNAME_FIELD_LENGTH)
|
||||||
|
private String initiator;
|
||||||
|
|
||||||
|
@Column(name = "remark", length = NamedEntity.DESCRIPTION_MAX_SIZE)
|
||||||
|
@Size(max = NamedEntity.DESCRIPTION_MAX_SIZE)
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default constructor needed for JPA entities.
|
||||||
|
*/
|
||||||
|
public JpaAutoConfirmationStatus() {
|
||||||
|
// Default constructor needed for JPA entities.
|
||||||
|
}
|
||||||
|
|
||||||
|
public JpaAutoConfirmationStatus(final String initiator, final String remark, final Target target) {
|
||||||
|
this.target = (JpaTarget) target;
|
||||||
|
this.initiator = StringUtils.isEmpty(initiator) ? null : initiator;
|
||||||
|
this.remark = StringUtils.isEmpty(remark) ? null : remark;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Target getTarget() {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getInitiator() {
|
||||||
|
return initiator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getActivatedAt() {
|
||||||
|
return getCreatedAt();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRemark() {
|
||||||
|
return remark;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String constructActionMessage() {
|
||||||
|
final String remarkMessage = StringUtils.hasText(remark) ? remark : "n/a";
|
||||||
|
final String formattedInitiator = StringUtils.hasText(initiator) ? initiator : "n/a";
|
||||||
|
final String createdByRolloutsUser = StringUtils.hasText(getCreatedBy()) ? getCreatedBy() : "n/a";
|
||||||
|
return String.format("Assignment automatically confirmed by initiator '%s'. %n%n" //
|
||||||
|
+ "Auto confirmation activated by system user: '%s' %n%n" //
|
||||||
|
+ "Remark: %s", formattedInitiator, createdByRolloutsUser, remarkMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "AutoConfirmationStatus [id=" + getId() + ", target=" + target.getControllerId() + ", initiator="
|
||||||
|
+ initiator + ", bosch_user=" + getCreatedBy() + ", activatedAt=" + getCreatedAt() + ", remark="
|
||||||
|
+ remark + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -118,6 +118,9 @@ public class JpaRolloutGroup extends AbstractJpaNamedEntity implements RolloutGr
|
|||||||
@Column(name = "target_percentage")
|
@Column(name = "target_percentage")
|
||||||
private float targetPercentage = 100;
|
private float targetPercentage = 100;
|
||||||
|
|
||||||
|
@Column(name = "confirmation_required")
|
||||||
|
private boolean confirmationRequired;
|
||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
private transient TotalTargetCountStatus totalTargetCountStatus;
|
private transient TotalTargetCountStatus totalTargetCountStatus;
|
||||||
|
|
||||||
@@ -251,6 +254,15 @@ public class JpaRolloutGroup extends AbstractJpaNamedEntity implements RolloutGr
|
|||||||
return targetPercentage;
|
return targetPercentage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setConfirmationRequired(final boolean confirmationRequired) {
|
||||||
|
this.confirmationRequired = confirmationRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isConfirmationRequired() {
|
||||||
|
return confirmationRequired;
|
||||||
|
}
|
||||||
|
|
||||||
public void setTargetPercentage(final float targetPercentage) {
|
public void setTargetPercentage(final float targetPercentage) {
|
||||||
this.targetPercentage = targetPercentage;
|
this.targetPercentage = targetPercentage;
|
||||||
}
|
}
|
||||||
@@ -279,7 +291,8 @@ public class JpaRolloutGroup extends AbstractJpaNamedEntity implements RolloutGr
|
|||||||
return "RolloutGroup [rollout=" + (rollout != null ? rollout.getId() : "") + ", status=" + status
|
return "RolloutGroup [rollout=" + (rollout != null ? rollout.getId() : "") + ", status=" + status
|
||||||
+ ", rolloutTargetGroup=" + rolloutTargetGroup + ", parent=" + parent + ", finishCondition="
|
+ ", rolloutTargetGroup=" + rolloutTargetGroup + ", parent=" + parent + ", finishCondition="
|
||||||
+ successCondition + ", finishExp=" + successConditionExp + ", errorCondition=" + errorCondition
|
+ successCondition + ", finishExp=" + successConditionExp + ", errorCondition=" + errorCondition
|
||||||
+ ", errorExp=" + errorConditionExp + ", getName()=" + getName() + ", getId()=" + getId() + "]";
|
+ ", errorExp=" + errorConditionExp + ", getName()=" + getName() + ", getId()=" + getId()
|
||||||
|
+ ", isConfirmationRequired()=" + isConfirmationRequired() + "]";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ import javax.persistence.ManyToMany;
|
|||||||
import javax.persistence.ManyToOne;
|
import javax.persistence.ManyToOne;
|
||||||
import javax.persistence.MapKeyColumn;
|
import javax.persistence.MapKeyColumn;
|
||||||
import javax.persistence.OneToMany;
|
import javax.persistence.OneToMany;
|
||||||
|
import javax.persistence.OneToOne;
|
||||||
|
import javax.persistence.PrimaryKeyJoinColumn;
|
||||||
import javax.persistence.Table;
|
import javax.persistence.Table;
|
||||||
import javax.persistence.UniqueConstraint;
|
import javax.persistence.UniqueConstraint;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
@@ -49,6 +51,7 @@ import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
|
|||||||
import org.eclipse.hawkbit.repository.jpa.model.helper.SecurityChecker;
|
import org.eclipse.hawkbit.repository.jpa.model.helper.SecurityChecker;
|
||||||
import org.eclipse.hawkbit.repository.jpa.model.helper.SecurityTokenGeneratorHolder;
|
import org.eclipse.hawkbit.repository.jpa.model.helper.SecurityTokenGeneratorHolder;
|
||||||
import org.eclipse.hawkbit.repository.model.Action;
|
import org.eclipse.hawkbit.repository.model.Action;
|
||||||
|
import org.eclipse.hawkbit.repository.model.AutoConfirmationStatus;
|
||||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||||
import org.eclipse.hawkbit.repository.model.PollStatus;
|
import org.eclipse.hawkbit.repository.model.PollStatus;
|
||||||
import org.eclipse.hawkbit.repository.model.Target;
|
import org.eclipse.hawkbit.repository.model.Target;
|
||||||
@@ -177,6 +180,10 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw
|
|||||||
@OneToMany(mappedBy = "target", fetch = FetchType.LAZY, targetEntity = JpaTargetMetadata.class)
|
@OneToMany(mappedBy = "target", fetch = FetchType.LAZY, targetEntity = JpaTargetMetadata.class)
|
||||||
private List<TargetMetadata> metadata;
|
private List<TargetMetadata> metadata;
|
||||||
|
|
||||||
|
@OneToOne(fetch = FetchType.LAZY, mappedBy = "target", orphanRemoval = true)
|
||||||
|
@PrimaryKeyJoinColumn
|
||||||
|
private JpaAutoConfirmationStatus autoConfirmationStatus;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
*
|
*
|
||||||
@@ -372,6 +379,15 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AutoConfirmationStatus getAutoConfirmationStatus() {
|
||||||
|
return autoConfirmationStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAutoConfirmationStatus(final JpaAutoConfirmationStatus autoConfirmationStatus) {
|
||||||
|
this.autoConfirmationStatus = autoConfirmationStatus;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Long getLastTargetQuery() {
|
public Long getLastTargetQuery() {
|
||||||
return lastTargetQuery;
|
return lastTargetQuery;
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ public class JpaTargetFilterQuery extends AbstractJpaTenantAwareBaseEntity
|
|||||||
@Column(name = "auto_assign_initiated_by", nullable = true, length = USERNAME_FIELD_LENGTH)
|
@Column(name = "auto_assign_initiated_by", nullable = true, length = USERNAME_FIELD_LENGTH)
|
||||||
private String autoAssignInitiatedBy;
|
private String autoAssignInitiatedBy;
|
||||||
|
|
||||||
|
@Column(name = "confirmation_required")
|
||||||
|
private boolean confirmationRequired;
|
||||||
|
|
||||||
public JpaTargetFilterQuery() {
|
public JpaTargetFilterQuery() {
|
||||||
// Default constructor for JPA.
|
// Default constructor for JPA.
|
||||||
}
|
}
|
||||||
@@ -97,14 +100,17 @@ public class JpaTargetFilterQuery extends AbstractJpaTenantAwareBaseEntity
|
|||||||
* of the {@link TargetFilterQuery}.
|
* of the {@link TargetFilterQuery}.
|
||||||
* @param autoAssignWeight
|
* @param autoAssignWeight
|
||||||
* of the {@link TargetFilterQuery}.
|
* of the {@link TargetFilterQuery}.
|
||||||
|
* @param confirmationRequired
|
||||||
|
* of the {@link TargetFilterQuery}.
|
||||||
*/
|
*/
|
||||||
public JpaTargetFilterQuery(final String name, final String query, final DistributionSet autoAssignDistributionSet,
|
public JpaTargetFilterQuery(final String name, final String query, final DistributionSet autoAssignDistributionSet,
|
||||||
final ActionType autoAssignActionType, final Integer autoAssignWeight) {
|
final ActionType autoAssignActionType, final Integer autoAssignWeight, final boolean confirmationRequired) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.query = query;
|
this.query = query;
|
||||||
this.autoAssignDistributionSet = (JpaDistributionSet) autoAssignDistributionSet;
|
this.autoAssignDistributionSet = (JpaDistributionSet) autoAssignDistributionSet;
|
||||||
this.autoAssignActionType = autoAssignActionType;
|
this.autoAssignActionType = autoAssignActionType;
|
||||||
this.autoAssignWeight = autoAssignWeight;
|
this.autoAssignWeight = autoAssignWeight;
|
||||||
|
this.confirmationRequired = confirmationRequired;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -160,6 +166,15 @@ public class JpaTargetFilterQuery extends AbstractJpaTenantAwareBaseEntity
|
|||||||
this.autoAssignInitiatedBy = autoAssignInitiatedBy;
|
this.autoAssignInitiatedBy = autoAssignInitiatedBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isConfirmationRequired() {
|
||||||
|
return confirmationRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfirmationRequired(final boolean confirmationRequired) {
|
||||||
|
this.confirmationRequired = confirmationRequired;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void fireCreateEvent(final DescriptorEvent descriptorEvent) {
|
public void fireCreateEvent(final DescriptorEvent descriptorEvent) {
|
||||||
EventPublisherHolder.getInstance().getEventPublisher().publishEvent(
|
EventPublisherHolder.getInstance().getEventPublisher().publishEvent(
|
||||||
@@ -177,4 +192,5 @@ public class JpaTargetFilterQuery extends AbstractJpaTenantAwareBaseEntity
|
|||||||
EventPublisherHolder.getInstance().getEventPublisher().publishEvent(new TargetFilterQueryDeletedEvent(
|
EventPublisherHolder.getInstance().getEventPublisher().publishEvent(new TargetFilterQueryDeletedEvent(
|
||||||
getTenant(), getId(), getClass(), EventPublisherHolder.getInstance().getApplicationId()));
|
getTenant(), getId(), getClass(), EventPublisherHolder.getInstance().getApplicationId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetFilterQueryCreate;
|
|||||||
import org.eclipse.hawkbit.repository.model.DeploymentRequest;
|
import org.eclipse.hawkbit.repository.model.DeploymentRequest;
|
||||||
import org.eclipse.hawkbit.repository.model.Rollout;
|
import org.eclipse.hawkbit.repository.model.Rollout;
|
||||||
import org.eclipse.hawkbit.security.SystemSecurityContext;
|
import org.eclipse.hawkbit.security.SystemSecurityContext;
|
||||||
|
import org.eclipse.hawkbit.utils.TenantConfigHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class to handle weight validation in Rollout, Auto Assignments, and
|
* Utility class to handle weight validation in Rollout, Auto Assignments, and
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
ALTER TABLE sp_rolloutgroup ADD COLUMN confirmation_required BOOLEAN;
|
||||||
|
UPDATE sp_rolloutgroup SET confirmation_required = 0;
|
||||||
|
|
||||||
|
ALTER TABLE sp_target_filter_query ADD COLUMN confirmation_required BOOLEAN;
|
||||||
|
UPDATE sp_target_filter_query SET confirmation_required = 0;
|
||||||
|
|
||||||
|
CREATE TABLE sp_target_conf_status
|
||||||
|
(
|
||||||
|
id BIGINT GENERATED always AS IDENTITY NOT NULL,
|
||||||
|
target_id bigint not null,
|
||||||
|
initiator VARCHAR(64),
|
||||||
|
remark VARCHAR(512),
|
||||||
|
created_at BIGINT,
|
||||||
|
created_by VARCHAR(64),
|
||||||
|
last_modified_at BIGINT,
|
||||||
|
last_modified_by VARCHAR(64),
|
||||||
|
optlock_revision BIGINT,
|
||||||
|
tenant VARCHAR(40) not null,
|
||||||
|
primary key (id)
|
||||||
|
);
|
||||||
|
ALTER TABLE sp_target_conf_status
|
||||||
|
ADD CONSTRAINT fk_target_auto_conf FOREIGN KEY (target_id) REFERENCES sp_target (id) ON DELETE CASCADE;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
ALTER TABLE sp_rolloutgroup ADD column confirmation_required BOOLEAN;
|
||||||
|
UPDATE sp_rolloutgroup SET confirmation_required = 0;
|
||||||
|
|
||||||
|
ALTER TABLE sp_target_filter_query ADD column confirmation_required BOOLEAN;
|
||||||
|
UPDATE sp_target_filter_query SET confirmation_required = 0;
|
||||||
|
|
||||||
|
create table sp_target_conf_status
|
||||||
|
(
|
||||||
|
id bigint not null auto_increment,
|
||||||
|
target_id bigint not null,
|
||||||
|
initiator varchar(64),
|
||||||
|
remark VARCHAR(512),
|
||||||
|
created_at bigint,
|
||||||
|
created_by varchar(64),
|
||||||
|
last_modified_at bigint,
|
||||||
|
last_modified_by varchar(64),
|
||||||
|
optlock_revision bigint,
|
||||||
|
tenant varchar(40) not null,
|
||||||
|
primary key (id)
|
||||||
|
);
|
||||||
|
ALTER TABLE sp_target_conf_status
|
||||||
|
ADD CONSTRAINT fk_target_auto_conf FOREIGN KEY (target_id) REFERENCES sp_target (id) ON DELETE CASCADE;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
ALTER TABLE sp_rolloutgroup ADD column confirmation_required BOOLEAN;
|
||||||
|
UPDATE sp_rolloutgroup SET confirmation_required = 0;
|
||||||
|
|
||||||
|
ALTER TABLE sp_target_filter_query ADD column confirmation_required BOOLEAN;
|
||||||
|
UPDATE sp_target_filter_query SET confirmation_required = 0;
|
||||||
|
|
||||||
|
create table sp_target_conf_status
|
||||||
|
(
|
||||||
|
id bigint not null auto_increment,
|
||||||
|
target_id bigint not null,
|
||||||
|
initiator varchar(64),
|
||||||
|
remark varchar(512),
|
||||||
|
created_at bigint,
|
||||||
|
created_by varchar(64),
|
||||||
|
last_modified_at bigint,
|
||||||
|
last_modified_by varchar(64),
|
||||||
|
optlock_revision bigint,
|
||||||
|
tenant varchar(40) not null,
|
||||||
|
primary key (id)
|
||||||
|
);
|
||||||
|
ALTER TABLE sp_target_conf_status
|
||||||
|
ADD CONSTRAINT fk_target_auto_conf FOREIGN KEY (target_id) REFERENCES sp_target (id) ON DELETE CASCADE;
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
ALTER TABLE sp_distribution_set ADD COLUMN valid BOOLEAN;
|
ALTER TABLE sp_distribution_set ADD COLUMN valid BOOLEAN;
|
||||||
|
|
||||||
UPDATE sp_distribution_set SET valid = 1;
|
UPDATE sp_distribution_set SET valid = TRUE;
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
ALTER TABLE sp_base_software_module ADD COLUMN encrypted BOOLEAN;
|
ALTER TABLE sp_base_software_module ADD COLUMN encrypted BOOLEAN;
|
||||||
|
|
||||||
UPDATE sp_base_software_module SET encrypted = 0;
|
UPDATE sp_base_software_module SET encrypted = FALSE;
|
||||||
@@ -1 +1 @@
|
|||||||
ALTER TABLE sp_target_type ALTER COLUMN name VARCHAR (128);
|
ALTER TABLE sp_target_type ALTER COLUMN name TYPE VARCHAR (128);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
ALTER TABLE sp_action_status ADD code INTEGER;
|
ALTER TABLE sp_action_status ADD COLUMN code INTEGER;
|
||||||
CREATE INDEX sp_idx_action_status_03
|
CREATE INDEX sp_idx_action_status_03
|
||||||
ON sp_action_status
|
ON sp_action_status
|
||||||
USING BTREE (tenant, code);
|
USING BTREE (tenant, code);
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
ALTER TABLE sp_rolloutgroup
|
||||||
|
ADD COLUMN confirmation_required BOOLEAN;
|
||||||
|
ALTER TABLE sp_target_filter_query
|
||||||
|
ADD COLUMN confirmation_required BOOLEAN;
|
||||||
|
|
||||||
|
UPDATE sp_rolloutgroup SET confirmation_required = FALSE;
|
||||||
|
UPDATE sp_target_filter_query SET confirmation_required = FALSE;
|
||||||
|
|
||||||
|
CREATE TABLE sp_target_conf_status
|
||||||
|
(
|
||||||
|
id BIGSERIAL,
|
||||||
|
target_id BIGINT NOT NULL,
|
||||||
|
initiator VARCHAR(64),
|
||||||
|
remark VARCHAR(512),
|
||||||
|
tenant VARCHAR(40) NOT NULL,
|
||||||
|
created_at BIGINT NOT NULL,
|
||||||
|
created_by VARCHAR(64) NOT NULL,
|
||||||
|
last_modified_at BIGINT NOT NULL,
|
||||||
|
last_modified_by VARCHAR(64) NOT NULL,
|
||||||
|
optlock_revision BIGINT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE sp_target_conf_status
|
||||||
|
ADD CONSTRAINT pk_sp_target_conf_status PRIMARY KEY (id);
|
||||||
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
ALTER TABLE sp_rolloutgroup ADD confirmation_required BIT DEFAULT 0;
|
||||||
|
ALTER TABLE sp_target_filter_query ADD confirmation_required BIT DEFAULT 0;
|
||||||
|
|
||||||
|
CREATE TABLE sp_target_conf_status
|
||||||
|
(
|
||||||
|
id NUMERIC(19) IDENTITY NOT NULL,
|
||||||
|
target_id NUMERIC(19) NOT NULL,
|
||||||
|
initiator VARCHAR(64),
|
||||||
|
remark VARCHAR(512),
|
||||||
|
tenant VARCHAR(40) NOT NULL,
|
||||||
|
created_at NUMERIC(19) NOT NULL,
|
||||||
|
created_by VARCHAR(64) NOT NULL,
|
||||||
|
last_modified_at NUMERIC(19) NOT NULL,
|
||||||
|
last_modified_by VARCHAR(64) NOT NULL,
|
||||||
|
optlock_revision INTEGER NULL,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
ALTER TABLE sp_target_conf_status
|
||||||
|
ADD CONSTRAINT fk_target_auto_conf FOREIGN KEY (target_id) REFERENCES sp_target (id) ON DELETE CASCADE;
|
||||||
@@ -44,8 +44,8 @@ public class RolloutEventTest extends AbstractRemoteEntityEventTest<Rollout> {
|
|||||||
.type("os").modules(Collections.singletonList(module.getId())));
|
.type("os").modules(Collections.singletonList(module.getId())));
|
||||||
|
|
||||||
return rolloutManagement.create(
|
return rolloutManagement.create(
|
||||||
entityFactory.rollout().create().name("exampleRollout").targetFilterQuery("controllerId==*").set(ds),
|
entityFactory.rollout().create().name("exampleRollout").targetFilterQuery("controllerId==*").set(ds), 5,
|
||||||
5, new RolloutGroupConditionBuilder().withDefaults()
|
false, new RolloutGroupConditionBuilder().withDefaults()
|
||||||
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, "10").build());
|
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, "10").build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,8 +84,8 @@ public class RolloutGroupEventTest extends AbstractRemoteEntityEventTest<Rollout
|
|||||||
.type("os").modules(Collections.singletonList(module.getId())));
|
.type("os").modules(Collections.singletonList(module.getId())));
|
||||||
|
|
||||||
final Rollout entity = rolloutManagement.create(
|
final Rollout entity = rolloutManagement.create(
|
||||||
entityFactory.rollout().create().name("exampleRollout").targetFilterQuery("controllerId==*").set(ds),
|
entityFactory.rollout().create().name("exampleRollout").targetFilterQuery("controllerId==*").set(ds), 5,
|
||||||
5, new RolloutGroupConditionBuilder().withDefaults()
|
false, new RolloutGroupConditionBuilder().withDefaults()
|
||||||
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, "10").build());
|
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, "10").build());
|
||||||
|
|
||||||
return rolloutGroupManagement.findByRollout(PAGE, entity.getId()).getContent().get(0);
|
return rolloutGroupManagement.findByRollout(PAGE, entity.getId()).getContent().get(0);
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ public class ConcurrentDistributionSetInvalidationTest extends AbstractJpaIntegr
|
|||||||
return rolloutManagement.create(entityFactory.rollout().create()
|
return rolloutManagement.create(entityFactory.rollout().create()
|
||||||
.name("verifyInvalidateDistributionSetWithLargeRolloutThrowsException").description("desc")
|
.name("verifyInvalidateDistributionSetWithLargeRolloutThrowsException").description("desc")
|
||||||
.targetFilterQuery("name==*").set(distributionSet).actionType(ActionType.FORCED),
|
.targetFilterQuery("name==*").set(distributionSet).actionType(ActionType.FORCED),
|
||||||
quotaManagement.getMaxRolloutGroupsPerRollout(), conditions);
|
quotaManagement.getMaxRolloutGroupsPerRollout(), false, conditions);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,315 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2022 Bosch.IO 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 static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.eclipse.hawkbit.repository.exception.AutoConfirmationAlreadyActiveException;
|
||||||
|
import org.eclipse.hawkbit.repository.exception.InvalidConfirmationFeedbackException;
|
||||||
|
import org.eclipse.hawkbit.repository.model.Action;
|
||||||
|
import org.eclipse.hawkbit.repository.model.Action.Status;
|
||||||
|
import org.eclipse.hawkbit.repository.model.DeploymentRequest;
|
||||||
|
import org.eclipse.hawkbit.repository.model.DeploymentRequestBuilder;
|
||||||
|
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||||
|
import org.eclipse.hawkbit.repository.model.Target;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import io.qameta.allure.Description;
|
||||||
|
import io.qameta.allure.Feature;
|
||||||
|
import io.qameta.allure.Story;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test class testing the functionality of triggering a deployment of
|
||||||
|
* {@link DistributionSet}s to {@link Target}s with AutoConfirmation active.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Feature("Component Tests - Repository")
|
||||||
|
@Story("Confirmation Management")
|
||||||
|
class ConfirmationManagementTest extends AbstractJpaIntegrationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Verify 'findActiveActionsWaitingConfirmation' method is filtering like expected")
|
||||||
|
void retrieveActionsWithConfirmationState() {
|
||||||
|
enableConfirmationFlow();
|
||||||
|
|
||||||
|
final String controllerId = testdataFactory.createTarget().getControllerId();
|
||||||
|
final Long dsId = testdataFactory.createDistributionSet().getId();
|
||||||
|
|
||||||
|
final List<Action> actions = assignDistributionSet(dsId, controllerId).getAssignedEntity();
|
||||||
|
assertThat(actions).hasSize(1);
|
||||||
|
|
||||||
|
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).hasSize(1)
|
||||||
|
.allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
|
||||||
|
final Long dsId2 = testdataFactory.createDistributionSet().getId();
|
||||||
|
// ds1 will be in canceling state afterwards
|
||||||
|
assignDistributionSet(dsId2, controllerId);
|
||||||
|
|
||||||
|
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).hasSize(1)
|
||||||
|
.allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Verify 'findActiveActionsWaitingConfirmation' method is filtering like expected with multi assignment active")
|
||||||
|
void retrieveActionsWithConfirmationStateInMultiAssignment() {
|
||||||
|
enableMultiAssignments();
|
||||||
|
enableConfirmationFlow();
|
||||||
|
|
||||||
|
final String controllerId = testdataFactory.createTarget().getControllerId();
|
||||||
|
final Long dsId = testdataFactory.createDistributionSet().getId();
|
||||||
|
|
||||||
|
final List<Action> actions = assignDistributionSet(dsId, controllerId).getAssignedEntity();
|
||||||
|
assertThat(actions).hasSize(1);
|
||||||
|
|
||||||
|
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).hasSize(1)
|
||||||
|
.allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
|
||||||
|
final Long dsId2 = testdataFactory.createDistributionSet().getId();
|
||||||
|
assignDistributionSet(dsId2, controllerId);
|
||||||
|
|
||||||
|
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).hasSize(2)
|
||||||
|
.allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
|
||||||
|
confirmationManagement.confirmAction(actions.get(0).getId(), null, null);
|
||||||
|
|
||||||
|
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).hasSize(1)
|
||||||
|
.allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION
|
||||||
|
&& Objects.equals(action.getDistributionSet().getId(), dsId2));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Verify confirming an action will put it to the running state")
|
||||||
|
void confirmedActionWillSwitchToRunningState() {
|
||||||
|
enableConfirmationFlow();
|
||||||
|
|
||||||
|
final String controllerId = testdataFactory.createTarget().getControllerId();
|
||||||
|
final Long dsId = testdataFactory.createDistributionSet().getId();
|
||||||
|
|
||||||
|
final List<Action> actions = assignDistributionSet(dsId, controllerId).getAssignedEntity();
|
||||||
|
assertThat(actions).hasSize(1).allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
|
||||||
|
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).hasSize(1)
|
||||||
|
.allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
assertThat(controllerManagement.findActionStatusByAction(PAGE, actions.get(0).getId())).hasSize(1)
|
||||||
|
.allMatch(status -> status.getStatus() == Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
|
||||||
|
final Action newAction = confirmationManagement.confirmAction(actions.get(0).getId(), null, null);
|
||||||
|
|
||||||
|
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).isEmpty();
|
||||||
|
|
||||||
|
// verify action in RUNNING state
|
||||||
|
assertThat(newAction.getStatus()).isEqualTo(Status.RUNNING);
|
||||||
|
|
||||||
|
// status entry RUNNING should be present in status history
|
||||||
|
assertThat(controllerManagement.findActionStatusByAction(PAGE, newAction.getId())).hasSize(2)
|
||||||
|
.anyMatch(status -> status.getStatus() == Status.RUNNING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Verify confirming an confirmed action will lead to a specific failure")
|
||||||
|
void confirmedActionCannotBeConfirmedAgain() {
|
||||||
|
enableConfirmationFlow();
|
||||||
|
|
||||||
|
final String controllerId = testdataFactory.createTarget().getControllerId();
|
||||||
|
final Long dsId = testdataFactory.createDistributionSet().getId();
|
||||||
|
|
||||||
|
final List<Action> actions = assignDistributionSet(dsId, controllerId).getAssignedEntity();
|
||||||
|
assertThat(actions).hasSize(1).allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
final Action newAction = confirmationManagement.confirmAction(actions.get(0).getId(), null, null);
|
||||||
|
// verify action in RUNNING state
|
||||||
|
assertThat(newAction.getStatus()).isEqualTo(Status.RUNNING);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> confirmationManagement.confirmAction(actions.get(0).getId(), null, null))
|
||||||
|
.isInstanceOf(InvalidConfirmationFeedbackException.class)
|
||||||
|
.matches(e -> ((InvalidConfirmationFeedbackException) e)
|
||||||
|
.getReason() == InvalidConfirmationFeedbackException.Reason.NOT_AWAITING_CONFIRMATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Verify confirming a closed action will lead to a specific failure")
|
||||||
|
void confirmedActionCannotBeGivenOnFinishedAction() {
|
||||||
|
enableConfirmationFlow();
|
||||||
|
final Action action = prepareFinishedUpdate();
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> confirmationManagement.confirmAction(action.getId(), null, null))
|
||||||
|
.isInstanceOf(InvalidConfirmationFeedbackException.class)
|
||||||
|
.matches(e -> ((InvalidConfirmationFeedbackException) e)
|
||||||
|
.getReason() == InvalidConfirmationFeedbackException.Reason.ACTION_CLOSED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Verify denying an action will leave it in WFC state")
|
||||||
|
void deniedActionWillStayInWfcState() {
|
||||||
|
enableConfirmationFlow();
|
||||||
|
|
||||||
|
final String controllerId = testdataFactory.createTarget().getControllerId();
|
||||||
|
final Long dsId = testdataFactory.createDistributionSet().getId();
|
||||||
|
|
||||||
|
final List<Action> actions = assignDistributionSet(dsId, controllerId).getAssignedEntity();
|
||||||
|
assertThat(actions).hasSize(1).allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).hasSize(1)
|
||||||
|
.allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
assertThat(controllerManagement.findActionStatusByAction(PAGE, actions.get(0).getId())).hasSize(1)
|
||||||
|
.allMatch(status -> status.getStatus() == Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
|
||||||
|
final Action newAction = confirmationManagement.denyAction(actions.get(0).getId(), null, null);
|
||||||
|
|
||||||
|
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).hasSize(1)
|
||||||
|
.allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
|
||||||
|
// verify action still in WFC state
|
||||||
|
assertThat(newAction.getStatus()).isEqualTo(Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
|
||||||
|
// no status entry RUNNING should be present in status history
|
||||||
|
assertThat(controllerManagement.findActionStatusByAction(PAGE, newAction.getId())).hasSize(2)
|
||||||
|
.noneMatch(status -> status.getStatus() == Status.RUNNING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Verify multiple actions in WFC state will be transferred in RUNNING state in case auto-confirmation is activated.")
|
||||||
|
void activateAutoConfirmationInMultiAssignment() {
|
||||||
|
enableMultiAssignments();
|
||||||
|
enableConfirmationFlow();
|
||||||
|
|
||||||
|
final String controllerId = testdataFactory.createTarget().getControllerId();
|
||||||
|
final Long dsId = testdataFactory.createDistributionSet().getId();
|
||||||
|
final Long dsId2 = testdataFactory.createDistributionSet().getId();
|
||||||
|
|
||||||
|
final List<Action> actions = assignDistributionSets(
|
||||||
|
Arrays.asList(toDeploymentRequest(controllerId, dsId), toDeploymentRequest(controllerId, dsId2)))
|
||||||
|
.stream().flatMap(s -> s.getAssignedEntity().stream()).collect(Collectors.toList());
|
||||||
|
assertThat(actions).hasSize(2);
|
||||||
|
|
||||||
|
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).hasSize(2)
|
||||||
|
.allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
|
||||||
|
confirmationManagement.activateAutoConfirmation(controllerId, null, null);
|
||||||
|
|
||||||
|
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).isEmpty();
|
||||||
|
|
||||||
|
assertThat(deploymentManagement.findActionsByTarget(controllerId, PAGE).getContent()).hasSize(2)
|
||||||
|
.allMatch(action -> action.getStatus() == Status.RUNNING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Verify action in WFC state will be transferred in RUNNING state in case auto-confirmation is activated.")
|
||||||
|
void activateAutoConfirmationOnActiveAction() {
|
||||||
|
enableConfirmationFlow();
|
||||||
|
|
||||||
|
final String controllerId = testdataFactory.createTarget().getControllerId();
|
||||||
|
final Long dsId = testdataFactory.createDistributionSet().getId();
|
||||||
|
|
||||||
|
// do assignment and verify
|
||||||
|
assertThat(assignDistributionSet(dsId, controllerId).getAssignedEntity()).hasSize(1);
|
||||||
|
|
||||||
|
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).hasSize(1)
|
||||||
|
.allMatch(action -> action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
|
||||||
|
confirmationManagement.activateAutoConfirmation(controllerId, null, null);
|
||||||
|
|
||||||
|
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).isEmpty();
|
||||||
|
|
||||||
|
assertThat(deploymentManagement.findActionsByTarget(controllerId, PAGE).getContent()).hasSize(1)
|
||||||
|
.allMatch(action -> action.getStatus() == Status.RUNNING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Verify created action after activating auto confirmation is directly in running state.")
|
||||||
|
void activateAutoConfirmationAndCreateAction() {
|
||||||
|
enableConfirmationFlow();
|
||||||
|
|
||||||
|
final String controllerId = testdataFactory.createTarget().getControllerId();
|
||||||
|
final Long dsId = testdataFactory.createDistributionSet().getId();
|
||||||
|
|
||||||
|
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).isEmpty();
|
||||||
|
|
||||||
|
confirmationManagement.activateAutoConfirmation(controllerId, null, null);
|
||||||
|
|
||||||
|
// do assignment and verify
|
||||||
|
assertThat(assignDistributionSet(dsId, controllerId).getAssignedEntity()).hasSize(1);
|
||||||
|
|
||||||
|
assertThat(confirmationManagement.findActiveActionsWaitingConfirmation(controllerId)).isEmpty();
|
||||||
|
|
||||||
|
assertThat(deploymentManagement.findActionsByTarget(controllerId, PAGE).getContent()).hasSize(1)
|
||||||
|
.allMatch(action -> action.getStatus() == Status.RUNNING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("getAutoConfirmationArguments")
|
||||||
|
@Description("Verify activating auto confirmation with different parameters")
|
||||||
|
void verifyAutoConfirmationActivationValues(final String initiator, final String remark) {
|
||||||
|
final String controllerId = testdataFactory.createTarget().getControllerId();
|
||||||
|
confirmationManagement.activateAutoConfirmation(controllerId, initiator, remark);
|
||||||
|
|
||||||
|
assertThat(targetManagement.getByControllerID(controllerId)).hasValueSatisfying(target -> {
|
||||||
|
assertThat(target.getAutoConfirmationStatus()).isNotNull()
|
||||||
|
.matches(status -> status.getTarget().getControllerId().equals(controllerId))
|
||||||
|
.matches(status -> Objects.equals(status.getInitiator(), initiator))
|
||||||
|
.matches(status -> Objects.equals(status.getCreatedBy(), "bumlux"))
|
||||||
|
.matches(status -> Objects.equals(status.getRemark(), remark)).satisfies(status -> {
|
||||||
|
final Instant activationTime = Instant.ofEpochMilli(status.getActivatedAt());
|
||||||
|
assertThat(activationTime).isAfterOrEqualTo(activationTime.minusSeconds(3L));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
confirmationManagement.deactivateAutoConfirmation(controllerId);
|
||||||
|
verifyAutoConfirmationIsDisabled(controllerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> getAutoConfirmationArguments() {
|
||||||
|
return Stream.of(Arguments.of("TestUser", "TestRemark"), Arguments.of("TestUser", null),
|
||||||
|
Arguments.of(null, "TestRemark"), Arguments.of(null, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Verify activating already active auto confirmation will throw exception.")
|
||||||
|
void verifyActivateAlreadyActiveAutoConfirmationThrowException() {
|
||||||
|
final String controllerId = testdataFactory.createTarget().getControllerId();
|
||||||
|
|
||||||
|
confirmationManagement.activateAutoConfirmation(controllerId, "any", "any");
|
||||||
|
assertThat(targetManagement.getByControllerID(controllerId))
|
||||||
|
.hasValueSatisfying(target -> assertThat(target.getAutoConfirmationStatus()).isNotNull());
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> confirmationManagement.activateAutoConfirmation(controllerId, "any", "any"))
|
||||||
|
.isInstanceOf(AutoConfirmationAlreadyActiveException.class)
|
||||||
|
.hasMessage("Auto confirmation is already active for device " + controllerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Verify disabling already disabled auto confirmation will not have any affect.")
|
||||||
|
void disableAlreadyDisabledAutoConfirmationHaveNoAffect() {
|
||||||
|
final String controllerId = testdataFactory.createTarget().getControllerId();
|
||||||
|
|
||||||
|
verifyAutoConfirmationIsDisabled(controllerId);
|
||||||
|
confirmationManagement.deactivateAutoConfirmation(controllerId);
|
||||||
|
verifyAutoConfirmationIsDisabled(controllerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyAutoConfirmationIsDisabled(final String controllerId) {
|
||||||
|
assertThat(targetManagement.getByControllerID(controllerId))
|
||||||
|
.hasValueSatisfying(target -> assertThat(target.getAutoConfirmationStatus()).isNull());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DeploymentRequest toDeploymentRequest(final String controllerId, final Long distributionSetId) {
|
||||||
|
return new DeploymentRequestBuilder(controllerId, distributionSetId).setConfirmationRequired(true).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -796,6 +796,8 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getUpdateStatus())
|
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getUpdateStatus())
|
||||||
.isEqualTo(TargetUpdateStatus.IN_SYNC);
|
.isEqualTo(TargetUpdateStatus.IN_SYNC);
|
||||||
|
|
||||||
|
assertThat(actionRepository.findById(action.getId()))
|
||||||
|
.hasValueSatisfying(a -> assertThat(a.getStatus()).isEqualTo(Status.FINISHED));
|
||||||
assertThat(actionStatusRepository.count()).isEqualTo(3);
|
assertThat(actionStatusRepository.count()).isEqualTo(3);
|
||||||
assertThat(controllerManagement.findActionStatusByAction(PAGE, action.getId()).getNumberOfElements())
|
assertThat(controllerManagement.findActionStatusByAction(PAGE, action.getId()).getNumberOfElements())
|
||||||
.isEqualTo(3);
|
.isEqualTo(3);
|
||||||
@@ -806,7 +808,8 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
+ "configured to accept them.")
|
+ "configured to accept them.")
|
||||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||||
@Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 1),
|
@Expect(type = ActionCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = ActionUpdatedEvent.class, count = 1),
|
||||||
@Expect(type = TargetUpdatedEvent.class, count = 2),
|
@Expect(type = TargetUpdatedEvent.class, count = 2),
|
||||||
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
|
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
|
||||||
@Expect(type = TargetAttributesRequestedEvent.class, count = 1),
|
@Expect(type = TargetAttributesRequestedEvent.class, count = 1),
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ package org.eclipse.hawkbit.repository.jpa;
|
|||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
import static org.eclipse.hawkbit.repository.model.Action.Status.RUNNING;
|
||||||
|
import static org.eclipse.hawkbit.repository.model.Action.Status.WAIT_FOR_CONFIRMATION;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
import java.util.AbstractMap.SimpleEntry;
|
import java.util.AbstractMap.SimpleEntry;
|
||||||
@@ -19,8 +21,10 @@ import java.util.Collection;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import javax.validation.ConstraintViolationException;
|
import javax.validation.ConstraintViolationException;
|
||||||
|
|
||||||
@@ -58,6 +62,7 @@ import org.eclipse.hawkbit.repository.model.Action.ActionType;
|
|||||||
import org.eclipse.hawkbit.repository.model.Action.Status;
|
import org.eclipse.hawkbit.repository.model.Action.Status;
|
||||||
import org.eclipse.hawkbit.repository.model.ActionStatus;
|
import org.eclipse.hawkbit.repository.model.ActionStatus;
|
||||||
import org.eclipse.hawkbit.repository.model.DeploymentRequest;
|
import org.eclipse.hawkbit.repository.model.DeploymentRequest;
|
||||||
|
import org.eclipse.hawkbit.repository.model.DeploymentRequestBuilder;
|
||||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||||
import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult;
|
import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult;
|
||||||
import org.eclipse.hawkbit.repository.model.DistributionSetTag;
|
import org.eclipse.hawkbit.repository.model.DistributionSetTag;
|
||||||
@@ -70,6 +75,8 @@ 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.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
@@ -284,7 +291,8 @@ class DeploymentManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
assignDistributionSet(cancelDs, targets).getAssignedEntity();
|
assignDistributionSet(cancelDs, targets).getAssignedEntity();
|
||||||
assertThat(deploymentManagement.countActionsAll()).isEqualTo(quotaManagement.getMaxTargetsPerAutoAssignment());
|
assertThat(deploymentManagement.countActionsAll()).isEqualTo(quotaManagement.getMaxTargetsPerAutoAssignment());
|
||||||
assignDistributionSet(cancelDs2, targets).getAssignedEntity();
|
assignDistributionSet(cancelDs2, targets).getAssignedEntity();
|
||||||
assertThat(deploymentManagement.countActionsAll()).isEqualTo(2L * quotaManagement.getMaxTargetsPerAutoAssignment());
|
assertThat(deploymentManagement.countActionsAll())
|
||||||
|
.isEqualTo(2L * quotaManagement.getMaxTargetsPerAutoAssignment());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -546,13 +554,13 @@ class DeploymentManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
final DistributionSet ds1 = testdataFactory.createDistributionSet("1");
|
final DistributionSet ds1 = testdataFactory.createDistributionSet("1");
|
||||||
assignDistributionSet(ds1, targets);
|
assignDistributionSet(ds1, targets);
|
||||||
|
|
||||||
assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_ACTIVE, Status.RUNNING);
|
assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_ACTIVE, RUNNING);
|
||||||
|
|
||||||
// Second assignment
|
// Second assignment
|
||||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("2");
|
final DistributionSet ds2 = testdataFactory.createDistributionSet("2");
|
||||||
assignDistributionSet(ds2, targets);
|
assignDistributionSet(ds2, targets);
|
||||||
|
|
||||||
assertDsExclusivelyAssignedToTargets(targets, ds2.getId(), STATE_ACTIVE, Status.RUNNING);
|
assertDsExclusivelyAssignedToTargets(targets, ds2.getId(), STATE_ACTIVE, RUNNING);
|
||||||
assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_INACTIVE, Status.CANCELED);
|
assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_INACTIVE, Status.CANCELED);
|
||||||
|
|
||||||
assertThat(targetManagement.findByAssignedDistributionSet(PAGE, ds2.getId()).getContent()).hasSize(10)
|
assertThat(targetManagement.findByAssignedDistributionSet(PAGE, ds2.getId()).getContent()).hasSize(10)
|
||||||
@@ -583,14 +591,14 @@ class DeploymentManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
final DistributionSet ds1 = testdataFactory.createDistributionSet("Multi-assign-1");
|
final DistributionSet ds1 = testdataFactory.createDistributionSet("Multi-assign-1");
|
||||||
assignDistributionSet(ds1.getId(), targetIds, 77);
|
assignDistributionSet(ds1.getId(), targetIds, 77);
|
||||||
|
|
||||||
assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_ACTIVE, Status.RUNNING);
|
assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_ACTIVE, RUNNING);
|
||||||
|
|
||||||
// Second assignment
|
// Second assignment
|
||||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("Multi-assign-2");
|
final DistributionSet ds2 = testdataFactory.createDistributionSet("Multi-assign-2");
|
||||||
assignDistributionSet(ds2.getId(), targetIds, 45);
|
assignDistributionSet(ds2.getId(), targetIds, 45);
|
||||||
|
|
||||||
assertDsExclusivelyAssignedToTargets(targets, ds2.getId(), STATE_ACTIVE, Status.RUNNING);
|
assertDsExclusivelyAssignedToTargets(targets, ds2.getId(), STATE_ACTIVE, RUNNING);
|
||||||
assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_ACTIVE, Status.RUNNING);
|
assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_ACTIVE, RUNNING);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertDsExclusivelyAssignedToTargets(final List<Target> targets, final long dsId, final boolean active,
|
private void assertDsExclusivelyAssignedToTargets(final List<Target> targets, final long dsId, final boolean active,
|
||||||
@@ -651,7 +659,8 @@ class DeploymentManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
void cancelMultiAssignmentActions() {
|
void cancelMultiAssignmentActions() {
|
||||||
final List<Target> targets = testdataFactory.createTargets(1);
|
final List<Target> targets = testdataFactory.createTargets(1);
|
||||||
final List<DistributionSet> distributionSets = testdataFactory.createDistributionSets(4);
|
final List<DistributionSet> distributionSets = testdataFactory.createDistributionSets(4);
|
||||||
final List<DeploymentRequest> deploymentRequests = createAssignmentRequests(distributionSets, targets, 34);
|
final List<DeploymentRequest> deploymentRequests = createAssignmentRequests(distributionSets, targets, 34,
|
||||||
|
false);
|
||||||
|
|
||||||
enableMultiAssignments();
|
enableMultiAssignments();
|
||||||
final List<DistributionSetAssignmentResult> results = deploymentManagement
|
final List<DistributionSetAssignmentResult> results = deploymentManagement
|
||||||
@@ -672,9 +681,15 @@ class DeploymentManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
|
|
||||||
protected List<DeploymentRequest> createAssignmentRequests(final Collection<DistributionSet> distributionSets,
|
protected List<DeploymentRequest> createAssignmentRequests(final Collection<DistributionSet> distributionSets,
|
||||||
final Collection<Target> targets, final int weight) {
|
final Collection<Target> targets, final int weight) {
|
||||||
|
return createAssignmentRequests(distributionSets, targets, weight, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<DeploymentRequest> createAssignmentRequests(final Collection<DistributionSet> distributionSets,
|
||||||
|
final Collection<Target> targets, final int weight, final boolean confirmationRequired) {
|
||||||
final List<DeploymentRequest> deploymentRequests = new ArrayList<>();
|
final List<DeploymentRequest> deploymentRequests = new ArrayList<>();
|
||||||
distributionSets.forEach(ds -> targets.forEach(target -> deploymentRequests.add(DeploymentManagement
|
distributionSets.forEach(ds -> targets.forEach(target -> deploymentRequests
|
||||||
.deploymentRequest(target.getControllerId(), ds.getId()).setWeight(weight).build())));
|
.add(DeploymentManagement.deploymentRequest(target.getControllerId(), ds.getId()).setWeight(weight)
|
||||||
|
.setConfirmationRequired(confirmationRequired).build())));
|
||||||
return deploymentRequests;
|
return deploymentRequests;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -713,7 +728,7 @@ class DeploymentManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
knownTargetIds.add(notExistingId);
|
knownTargetIds.add(notExistingId);
|
||||||
|
|
||||||
final List<DistributionSetAssignmentResult> assignDistributionSetsResults = assignDistributionSetToTargets(
|
final List<DistributionSetAssignmentResult> assignDistributionSetsResults = assignDistributionSetToTargets(
|
||||||
createdDs, knownTargetIds);
|
createdDs, knownTargetIds, false);
|
||||||
|
|
||||||
for (final DistributionSetAssignmentResult assignDistributionSetsResult : assignDistributionSetsResults) {
|
for (final DistributionSetAssignmentResult assignDistributionSetsResult : assignDistributionSetsResults) {
|
||||||
assertThat(assignDistributionSetsResult.getAlreadyAssigned()).isZero();
|
assertThat(assignDistributionSetsResult.getAlreadyAssigned()).isZero();
|
||||||
@@ -722,12 +737,152 @@ class DeploymentManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(booleans = { true, false })
|
||||||
|
@Description("Assignments with confirmation flow active will result in actions in 'WAIT_FOR_CONFIRMATION' state")
|
||||||
|
void assignmentWithConfirmationFlowActive(final boolean confirmationRequired) {
|
||||||
|
final List<String> controllerIds = testdataFactory.createTargets(1).stream().map(Target::getControllerId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
final DistributionSet distributionSet = testdataFactory.createDistributionSet();
|
||||||
|
|
||||||
|
enableConfirmationFlow();
|
||||||
|
List<DistributionSetAssignmentResult> results = assignDistributionSetToTargets(distributionSet, controllerIds,
|
||||||
|
confirmationRequired);
|
||||||
|
|
||||||
|
assertThat(getResultingActionCount(results)).isEqualTo(controllerIds.size());
|
||||||
|
|
||||||
|
controllerIds.forEach(controllerId -> {
|
||||||
|
actionRepository.findByTargetControllerId(PAGE, controllerId).forEach(action -> {
|
||||||
|
assertThat(action.getDistributionSet().getId()).isIn(distributionSet.getId());
|
||||||
|
assertThat(action.getInitiatedBy()).as("Should be Initiated by current user")
|
||||||
|
.isEqualTo(tenantAware.getCurrentUsername());
|
||||||
|
if (confirmationRequired) {
|
||||||
|
assertThat(action.getStatus()).isEqualTo(Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
} else {
|
||||||
|
assertThat(action.getStatus()).isEqualTo(RUNNING);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(booleans = { true, false })
|
||||||
|
@Description("Verify auto confirmation assignments and check action status with messages")
|
||||||
|
void assignmentWithAutoConfirmationWillBeHandledCorrectly(final boolean confirmationRequired) {
|
||||||
|
enableConfirmationFlow();
|
||||||
|
|
||||||
|
final Target target = testdataFactory.createTarget();
|
||||||
|
assertThat(target.getAutoConfirmationStatus()).isNull();
|
||||||
|
|
||||||
|
confirmationManagement.activateAutoConfirmation(target.getControllerId(), "not_bumlux", "my personal remark");
|
||||||
|
|
||||||
|
assertThat(targetManagement.getByControllerID(target.getControllerId()))
|
||||||
|
.hasValueSatisfying(t -> assertThat(t.getAutoConfirmationStatus()).isNotNull());
|
||||||
|
|
||||||
|
final DistributionSet distributionSet = testdataFactory.createDistributionSet();
|
||||||
|
|
||||||
|
assignDistributionSets(Collections
|
||||||
|
.singletonList(new DeploymentRequestBuilder(target.getControllerId(), distributionSet.getId())
|
||||||
|
.setConfirmationRequired(confirmationRequired).build()));
|
||||||
|
|
||||||
|
assertThat(deploymentManagement.findActionsByTarget(target.getControllerId(), PAGE).getContent()).hasSize(1)
|
||||||
|
.allSatisfy(action -> {
|
||||||
|
assertThat(action.getStatus()).isEqualTo(RUNNING);
|
||||||
|
assertThat(actionStatusRepository.getByActionId(PAGE, action.getId())).hasSize(1)
|
||||||
|
.allSatisfy(status -> {
|
||||||
|
final JpaActionStatus actionStatus = (JpaActionStatus) status;
|
||||||
|
assertThat(actionStatus.getStatus()).isEqualTo(WAIT_FOR_CONFIRMATION);
|
||||||
|
if (confirmationRequired) {
|
||||||
|
// confirmation of assignment is basically required, but active
|
||||||
|
// auto-confirmation will perform the confirmation
|
||||||
|
assertThat(actionStatus.getMessages())
|
||||||
|
.contains("Assignment initiated by user 'bumlux'")
|
||||||
|
.contains("Assignment automatically confirmed by initiator 'not_bumlux'. \n"
|
||||||
|
+ "\n" + "Auto confirmation activated by system user: 'bumlux' \n"
|
||||||
|
+ "\n" + "Remark: my personal remark");
|
||||||
|
} else {
|
||||||
|
// assignment never required confirmation, auto-confirmation will not be
|
||||||
|
// applied.
|
||||||
|
// assignment initiator has confirmed the action already
|
||||||
|
assertThat(actionStatus.getMessages())
|
||||||
|
.contains("Assignment initiated by user 'bumlux'")
|
||||||
|
.contains("Assignment confirmed by initiator [bumlux].");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Multiple assignments with confirmation flow active will result in correct cancel behaviour")
|
||||||
|
void multipleAssignmentWithConfirmationFlowActiveVerifyCancelBehaviour() {
|
||||||
|
final Target target = testdataFactory.createTarget("firstDevice");
|
||||||
|
final DistributionSet firstDs = testdataFactory.createDistributionSet();
|
||||||
|
final DistributionSet secondDs = testdataFactory.createDistributionSet();
|
||||||
|
|
||||||
|
enableConfirmationFlow();
|
||||||
|
final List<Action> resultActions = assignDistributionSet(firstDs.getId(), target.getControllerId())
|
||||||
|
.getAssignedEntity();
|
||||||
|
assertThat(resultActions).hasSize(1);
|
||||||
|
|
||||||
|
assertThat(resultActions.get(0)).satisfies(action -> {
|
||||||
|
assertThat(action.getDistributionSet().getId()).isEqualTo(firstDs.getId());
|
||||||
|
assertThat(action.getStatus()).isEqualTo(Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<Action> resultActions2 = assignDistributionSet(secondDs.getId(), target.getControllerId())
|
||||||
|
.getAssignedEntity();
|
||||||
|
|
||||||
|
assertThat(resultActions2).hasSize(1);
|
||||||
|
assertThat(resultActions2.get(0)).satisfies(action -> {
|
||||||
|
assertThat(action.getDistributionSet().getId()).isEqualTo(secondDs.getId());
|
||||||
|
assertThat(action.getStatus()).isEqualTo(Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<Action> actions = actionRepository.findByTargetControllerId(PAGE, target.getControllerId())
|
||||||
|
.getContent();
|
||||||
|
assertThat(actions).hasSize(2)
|
||||||
|
.anyMatch(action -> Objects.equals(action.getDistributionSet().getId(), firstDs.getId())
|
||||||
|
&& action.getStatus() == Status.CANCELING)
|
||||||
|
.anyMatch(action -> Objects.equals(action.getDistributionSet().getId(), secondDs.getId())
|
||||||
|
&& action.getStatus() == Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Assignments with confirmation flow deactivated will result in actions in only in 'RUNNING' state")
|
||||||
|
void verifyConfirmationRequiredFlagHaveNoInfluenceIfFlowIsDeactivated() {
|
||||||
|
final List<String> targets1 = testdataFactory.createTargets("group1", 1).stream().map(Target::getControllerId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
final List<String> targets2 = testdataFactory.createTargets("group2", 1).stream().map(Target::getControllerId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
final DistributionSet distributionSet = testdataFactory.createDistributionSet();
|
||||||
|
|
||||||
|
final List<DistributionSetAssignmentResult> results = Stream
|
||||||
|
.concat(assignDistributionSetToTargets(distributionSet, targets1, true).stream(), //
|
||||||
|
assignDistributionSetToTargets(distributionSet, targets2, false).stream()) //
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
final List<String> controllerIds = Stream.concat(targets1.stream(), targets2.stream())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
assertThat(getResultingActionCount(results)).isEqualTo(controllerIds.size());
|
||||||
|
|
||||||
|
controllerIds.forEach(controllerId -> {
|
||||||
|
actionRepository.findByTargetControllerId(PAGE, controllerId).forEach(action -> {
|
||||||
|
assertThat(action.getDistributionSet().getId()).isIn(distributionSet.getId());
|
||||||
|
assertThat(action.getInitiatedBy()).as("Should be Initiated by current user")
|
||||||
|
.isEqualTo(tenantAware.getCurrentUsername());
|
||||||
|
assertThat(action.getStatus()).isEqualTo(RUNNING);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private List<DistributionSetAssignmentResult> assignDistributionSetToTargets(final DistributionSet distributionSet,
|
private List<DistributionSetAssignmentResult> assignDistributionSetToTargets(final DistributionSet distributionSet,
|
||||||
final Iterable<String> targetIds) {
|
final Iterable<String> targetIds, final boolean confirmationRequired) {
|
||||||
final List<DeploymentRequest> deploymentRequests = new ArrayList<>();
|
final List<DeploymentRequest> deploymentRequests = new ArrayList<>();
|
||||||
for (final String controllerId : targetIds) {
|
for (final String controllerId : targetIds) {
|
||||||
deploymentRequests.add(new DeploymentRequest(controllerId, distributionSet.getId(), ActionType.FORCED, 0,
|
deploymentRequests.add(new DeploymentRequest(controllerId, distributionSet.getId(), ActionType.FORCED, 0,
|
||||||
null, null, null, null));
|
null, null, null, null, confirmationRequired));
|
||||||
}
|
}
|
||||||
return deploymentManagement.assignDistributionSets(deploymentRequests);
|
return deploymentManagement.assignDistributionSets(deploymentRequests);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import java.util.Optional;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import javax.validation.ConstraintViolationException;
|
import javax.validation.ConstraintViolationException;
|
||||||
import javax.validation.ValidationException;
|
import javax.validation.ValidationException;
|
||||||
@@ -31,6 +32,7 @@ import org.assertj.core.api.Assertions;
|
|||||||
import org.assertj.core.api.Condition;
|
import org.assertj.core.api.Condition;
|
||||||
import org.awaitility.Awaitility;
|
import org.awaitility.Awaitility;
|
||||||
import org.awaitility.Duration;
|
import org.awaitility.Duration;
|
||||||
|
import org.eclipse.hawkbit.repository.Identifiable;
|
||||||
import org.eclipse.hawkbit.repository.OffsetBasedPageRequest;
|
import org.eclipse.hawkbit.repository.OffsetBasedPageRequest;
|
||||||
import org.eclipse.hawkbit.repository.builder.RolloutCreate;
|
import org.eclipse.hawkbit.repository.builder.RolloutCreate;
|
||||||
import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate;
|
import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate;
|
||||||
@@ -81,6 +83,9 @@ import org.eclipse.hawkbit.repository.test.util.WithSpringAuthorityRule;
|
|||||||
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
|
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
@@ -139,6 +144,42 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
assertThat(rolloutCreatedAction.getStatus()).isEqualTo(Status.FINISHED);
|
assertThat(rolloutCreatedAction.getStatus()).isEqualTo(Status.FINISHED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("simpleRolloutsPossibilities")
|
||||||
|
@Description("Verifies that action states are correctly initialized after starting a rollout with different options in regard to the confirmation.")
|
||||||
|
void runRolloutWithConfirmationFlagAndCoonfirmationFlowOptions(final boolean confirmationFlowActive,
|
||||||
|
final boolean confirmationRequired, final Status expectedStatus) {
|
||||||
|
// manually assign distribution set to target
|
||||||
|
final String knownControllerId = "controller12345";
|
||||||
|
final DistributionSet knownDistributionSet = testdataFactory.createDistributionSet();
|
||||||
|
testdataFactory.createTarget(knownControllerId);
|
||||||
|
|
||||||
|
if (confirmationFlowActive) {
|
||||||
|
enableConfirmationFlow();
|
||||||
|
}
|
||||||
|
|
||||||
|
// create rollout with the same distribution set already assigned
|
||||||
|
// start rollout
|
||||||
|
final Rollout rollout = testdataFactory.createRolloutByVariables("rolloutNotCancelRunningAction", "description",
|
||||||
|
1, "name==*", knownDistributionSet, "50", "5", confirmationRequired);
|
||||||
|
rolloutManagement.start(rollout.getId());
|
||||||
|
rolloutManagement.handleRollouts();
|
||||||
|
|
||||||
|
// verify that manually created action is still running and action
|
||||||
|
// created from rollout is finished
|
||||||
|
final List<Action> actionsByKnownTarget = deploymentManagement.findActionsByTarget(knownControllerId, PAGE)
|
||||||
|
.getContent();
|
||||||
|
assertThat(actionsByKnownTarget).hasSize(1);
|
||||||
|
assertThat(actionsByKnownTarget.get(0).getStatus()).isEqualTo(expectedStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> simpleRolloutsPossibilities() {
|
||||||
|
return Stream.of(Arguments.of(true, true, Status.WAIT_FOR_CONFIRMATION), //
|
||||||
|
Arguments.of(true, false, Status.RUNNING), //
|
||||||
|
Arguments.of(false, true, Status.RUNNING), //
|
||||||
|
Arguments.of(false, false, Status.RUNNING));//
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Description("Verifies that a running action is auto canceled by a rollout which assigns another distribution-set.")
|
@Description("Verifies that a running action is auto canceled by a rollout which assigns another distribution-set.")
|
||||||
void rolloutAssignsNewDistributionSetAndAutoCloseActiveActions() {
|
void rolloutAssignsNewDistributionSetAndAutoCloseActiveActions() {
|
||||||
@@ -1369,7 +1410,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
.targetFilterQuery("controllerId==" + targetPrefixName + "-*").set(distributionSet);
|
.targetFilterQuery("controllerId==" + targetPrefixName + "-*").set(distributionSet);
|
||||||
|
|
||||||
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
|
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
|
||||||
.isThrownBy(() -> rolloutManagement.create(rollout, amountGroups, conditions));
|
.isThrownBy(() -> rolloutManagement.create(rollout, amountGroups, false, conditions));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -1540,6 +1581,117 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Verify rollout execution with advanced group definition and confirmation flow active.")
|
||||||
|
void createRolloutWithGroupDefinitionAndConfirmationFlowActive() {
|
||||||
|
final String rolloutName = "rolloutTest4";
|
||||||
|
|
||||||
|
final int amountTargetsInGroup1 = 10;
|
||||||
|
final int amountTargetsInGroup2 = 20;
|
||||||
|
|
||||||
|
final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder().withDefaults().build();
|
||||||
|
// Generate Targets for group 2
|
||||||
|
final RolloutCreate rolloutcreate = generateTargetsAndRollout(rolloutName, amountTargetsInGroup2);
|
||||||
|
|
||||||
|
// Generate Targets for group 1
|
||||||
|
testdataFactory.createTargets(amountTargetsInGroup1, rolloutName + "-gr1-", rolloutName);
|
||||||
|
|
||||||
|
final List<RolloutGroupCreate> rolloutGroups = new ArrayList<>(3);
|
||||||
|
rolloutGroups.add(generateRolloutGroup(0, 100, "id==" + rolloutName + "-gr1-*", true));
|
||||||
|
rolloutGroups.add(generateRolloutGroup(1, 100, null, false));
|
||||||
|
|
||||||
|
// enable confirmation flow
|
||||||
|
enableConfirmationFlow();
|
||||||
|
|
||||||
|
final Long rolloutId = rolloutManagement.create(rolloutcreate, rolloutGroups, conditions).getId();
|
||||||
|
|
||||||
|
assertThat(getRollout(rolloutId)).satisfies(rollout -> {
|
||||||
|
assertThat(rollout.getStatus()).isEqualTo(RolloutStatus.CREATING);
|
||||||
|
for (final RolloutGroup group : rolloutGroupManagement.findByRollout(PAGE, rollout.getId()).getContent()) {
|
||||||
|
assertThat(group.getStatus()).isEqualTo(RolloutGroupStatus.CREATING);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// first handle iteration will put rollout in ready state
|
||||||
|
rolloutManagement.handleRollouts();
|
||||||
|
|
||||||
|
assertThat(getRollout(rolloutId)).satisfies(rollout -> {
|
||||||
|
assertThat(rollout.getStatus()).isEqualTo(RolloutStatus.READY);
|
||||||
|
assertThat(rollout.getTotalTargets()).isEqualTo(amountTargetsInGroup1 + amountTargetsInGroup2);
|
||||||
|
});
|
||||||
|
|
||||||
|
// verify created rollout groups
|
||||||
|
final List<Long> rolloutGroupIds = rolloutGroupManagement.findByRollout(PAGE, rolloutId).getContent().stream()
|
||||||
|
.map(Identifiable::getId).collect(Collectors.toList());
|
||||||
|
assertThat(rolloutGroupIds).hasSize(2);
|
||||||
|
assertRolloutGroup(rolloutGroupIds.get(0), RolloutGroupStatus.READY, true, amountTargetsInGroup1, null);
|
||||||
|
assertRolloutGroup(rolloutGroupIds.get(1), RolloutGroupStatus.READY, false, amountTargetsInGroup2, null);
|
||||||
|
|
||||||
|
// start rollout
|
||||||
|
rolloutManagement.start(rolloutId);
|
||||||
|
rolloutManagement.handleRollouts();
|
||||||
|
|
||||||
|
// verify rollout started. Check groups are in right state.
|
||||||
|
// Group 1 should be in WFC state, since confirmation is required here.
|
||||||
|
assertRolloutGroup(rolloutGroupIds.get(0), RolloutGroupStatus.RUNNING, true, amountTargetsInGroup1,
|
||||||
|
Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
assertRolloutGroup(rolloutGroupIds.get(1), RolloutGroupStatus.SCHEDULED, false, amountTargetsInGroup2,
|
||||||
|
Status.SCHEDULED);
|
||||||
|
|
||||||
|
// cancel execution of all action of group 1 to trigger second group
|
||||||
|
forceQuitAllActionsOfRolloutGroup(rolloutGroupIds.get(0));
|
||||||
|
|
||||||
|
rolloutManagement.handleRollouts();
|
||||||
|
assertRolloutGroup(rolloutGroupIds.get(0), RolloutGroupStatus.FINISHED, true, amountTargetsInGroup1,
|
||||||
|
Status.CANCELED);
|
||||||
|
assertRolloutGroup(rolloutGroupIds.get(1), RolloutGroupStatus.SCHEDULED, false, amountTargetsInGroup2,
|
||||||
|
Status.SCHEDULED);
|
||||||
|
|
||||||
|
// verify actions of second rule are directly in RUNNING state, since
|
||||||
|
// confirmation is not required for this group
|
||||||
|
rolloutManagement.handleRollouts();
|
||||||
|
assertRolloutGroup(rolloutGroupIds.get(0), RolloutGroupStatus.FINISHED, true, amountTargetsInGroup1,
|
||||||
|
Status.CANCELED);
|
||||||
|
assertRolloutGroup(rolloutGroupIds.get(1), RolloutGroupStatus.RUNNING, false, amountTargetsInGroup2,
|
||||||
|
Status.RUNNING);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertRolloutGroup(final long rolloutGroupId, final RolloutGroupStatus status,
|
||||||
|
final boolean isConfirmationRequired, final long totalTargets, final Status actionStatusToCheck) {
|
||||||
|
assertThat(rolloutGroupManagement.get(rolloutGroupId)).hasValueSatisfying(rolloutGroup -> {
|
||||||
|
assertThat(rolloutGroup.getStatus()).isEqualTo(status);
|
||||||
|
assertThat(rolloutGroup.isConfirmationRequired()).isEqualTo(isConfirmationRequired);
|
||||||
|
assertThat(rolloutGroup.getTotalTargets()).isEqualTo(totalTargets);
|
||||||
|
if (actionStatusToCheck != null) {
|
||||||
|
assertAllActionOfRolloutGroupHavingStatus(rolloutGroup.getId(), actionStatusToCheck);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertAllActionOfRolloutGroupHavingStatus(final long rolloutGroupId, final Status status) {
|
||||||
|
final List<Target> targets = rolloutGroupManagement.findTargetsOfRolloutGroup(PAGE, rolloutGroupId)
|
||||||
|
.getContent();
|
||||||
|
targets.forEach(target -> {
|
||||||
|
final List<Action> activeActions = deploymentManagement
|
||||||
|
.findActionsByTarget(target.getControllerId(), PAGE).getContent();
|
||||||
|
assertThat(activeActions).hasSize(1);
|
||||||
|
assertThat(activeActions.get(0).getStatus()).isEqualTo(status);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void forceQuitAllActionsOfRolloutGroup(final long rolloutGroupId) {
|
||||||
|
final List<Target> targets = rolloutGroupManagement.findTargetsOfRolloutGroup(PAGE, rolloutGroupId)
|
||||||
|
.getContent();
|
||||||
|
targets.forEach(target -> {
|
||||||
|
deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId()).getContent().stream().map(Identifiable::getId)
|
||||||
|
.forEach(actionId -> {
|
||||||
|
deploymentManagement.cancelAction(actionId);
|
||||||
|
deploymentManagement.forceQuitAction(actionId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Description("Verify rollout creation fails if group definition does not address all targets")
|
@Description("Verify rollout creation fails if group definition does not address all targets")
|
||||||
void createRolloutWithGroupsNotMatchingTargets() {
|
void createRolloutWithGroupsNotMatchingTargets() {
|
||||||
@@ -1593,7 +1745,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
final RolloutCreate rollout = generateTargetsAndRollout(rolloutName, targets);
|
final RolloutCreate rollout = generateTargetsAndRollout(rolloutName, targets);
|
||||||
|
|
||||||
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
|
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
|
||||||
.isThrownBy(() -> rolloutManagement.create(rollout, maxGroups + 1, conditions))
|
.isThrownBy(() -> rolloutManagement.create(rollout, maxGroups + 1, false, conditions))
|
||||||
.withMessageContaining("not be greater than " + maxGroups);
|
.withMessageContaining("not be greater than " + maxGroups);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1615,7 +1767,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
final RolloutCreate rolloutToCreate = entityFactory.rollout().create().name(rolloutName)
|
final RolloutCreate rolloutToCreate = entityFactory.rollout().create().name(rolloutName)
|
||||||
.description("some description").targetFilterQuery("id==" + rolloutName + "-*").set(distributionSet);
|
.description("some description").targetFilterQuery("id==" + rolloutName + "-*").set(distributionSet);
|
||||||
|
|
||||||
Rollout myRollout = rolloutManagement.create(rolloutToCreate, amountGroups, conditions);
|
Rollout myRollout = rolloutManagement.create(rolloutToCreate, amountGroups, false, conditions);
|
||||||
myRollout = getRollout(myRollout.getId());
|
myRollout = getRollout(myRollout.getId());
|
||||||
|
|
||||||
assertThat(myRollout.getStatus()).isEqualTo(RolloutStatus.CREATING);
|
assertThat(myRollout.getStatus()).isEqualTo(RolloutStatus.CREATING);
|
||||||
@@ -1787,7 +1939,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
.name(prefixRolloutRunning + "-testRollout").targetFilterQuery("name==" + randomString + "*")
|
.name(prefixRolloutRunning + "-testRollout").targetFilterQuery("name==" + randomString + "*")
|
||||||
.set(testDs);
|
.set(testDs);
|
||||||
|
|
||||||
Rollout rolloutRunning = rolloutManagement.create(rolloutRunningCreate, 1, conditions);
|
Rollout rolloutRunning = rolloutManagement.create(rolloutRunningCreate, 1, false, conditions);
|
||||||
// Let the executor handle created Rollout
|
// Let the executor handle created Rollout
|
||||||
rolloutManagement.handleRollouts();
|
rolloutManagement.handleRollouts();
|
||||||
// start the rollout, so it has active running actions and a group which
|
// start the rollout, so it has active running actions and a group which
|
||||||
@@ -1799,7 +1951,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
final String prefixRolloutReady = randomString + "2";
|
final String prefixRolloutReady = randomString + "2";
|
||||||
final RolloutCreate rolloutReadyCreate = entityFactory.rollout().create()
|
final RolloutCreate rolloutReadyCreate = entityFactory.rollout().create()
|
||||||
.name(prefixRolloutReady + "-testRollout").targetFilterQuery("name==" + randomString + "*").set(testDs);
|
.name(prefixRolloutReady + "-testRollout").targetFilterQuery("name==" + randomString + "*").set(testDs);
|
||||||
Rollout rolloutReady = rolloutManagement.create(rolloutReadyCreate, 1, conditions);
|
Rollout rolloutReady = rolloutManagement.create(rolloutReadyCreate, 1, false, conditions);
|
||||||
// Let the executor handle created Rollout
|
// Let the executor handle created Rollout
|
||||||
rolloutManagement.handleRollouts();
|
rolloutManagement.handleRollouts();
|
||||||
rolloutReady = reloadRollout(rolloutReady);
|
rolloutReady = reloadRollout(rolloutReady);
|
||||||
@@ -1959,7 +2111,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
final RolloutCreate rolloutToCreate = entityFactory.rollout().create().name(rolloutName)
|
final RolloutCreate rolloutToCreate = entityFactory.rollout().create().name(rolloutName)
|
||||||
.targetFilterQuery("name==*").set(testDs);
|
.targetFilterQuery("name==*").set(testDs);
|
||||||
|
|
||||||
final Rollout createdRollout = rolloutManagement.create(rolloutToCreate, 1, conditions);
|
final Rollout createdRollout = rolloutManagement.create(rolloutToCreate, 1, false, conditions);
|
||||||
|
|
||||||
// Let the executor handle created Rollout
|
// Let the executor handle created Rollout
|
||||||
rolloutManagement.handleRollouts();
|
rolloutManagement.handleRollouts();
|
||||||
@@ -1982,8 +2134,14 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
|
|
||||||
private RolloutGroupCreate generateRolloutGroup(final int index, final Integer percentage,
|
private RolloutGroupCreate generateRolloutGroup(final int index, final Integer percentage,
|
||||||
final String targetFilter) {
|
final String targetFilter) {
|
||||||
|
return generateRolloutGroup(index, percentage, targetFilter, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RolloutGroupCreate generateRolloutGroup(final int index, final Integer percentage,
|
||||||
|
final String targetFilter, final boolean confirmationRequired) {
|
||||||
return entityFactory.rolloutGroup().create().name("Group" + index).description("Group" + index + "desc")
|
return entityFactory.rolloutGroup().create().name("Group" + index).description("Group" + index + "desc")
|
||||||
.targetPercentage(Float.valueOf(percentage)).targetFilterQuery(targetFilter);
|
.targetPercentage(Float.valueOf(percentage)).targetFilterQuery(targetFilter)
|
||||||
|
.confirmationRequired(confirmationRequired);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RolloutCreate generateTargetsAndRollout(final String rolloutName, final int amountTargetsForRollout) {
|
private RolloutCreate generateTargetsAndRollout(final String rolloutName, final int amountTargetsForRollout) {
|
||||||
@@ -2033,7 +2191,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
testdataFactory.createTargets(amountTargetsForRollout, targetPrefixName + "-", targetPrefixName);
|
testdataFactory.createTargets(amountTargetsForRollout, targetPrefixName + "-", targetPrefixName);
|
||||||
return testdataFactory.createRolloutByVariables(rolloutName, rolloutName + "description", groupSize,
|
return testdataFactory.createRolloutByVariables(rolloutName, rolloutName + "description", groupSize,
|
||||||
"controllerId==" + targetPrefixName + "-*", dsForRolloutTwo, successCondition, errorCondition,
|
"controllerId==" + targetPrefixName + "-*", dsForRolloutTwo, successCondition, errorCondition,
|
||||||
Action.ActionType.FORCED, weight);
|
Action.ActionType.FORCED, weight, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int changeStatusForAllRunningActions(final Rollout rollout, final Status status) {
|
private int changeStatusForAllRunningActions(final Rollout rollout, final Status status) {
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
|
|||||||
import org.eclipse.hawkbit.repository.model.TargetType;
|
import org.eclipse.hawkbit.repository.model.TargetType;
|
||||||
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
|
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.domain.Slice;
|
import org.springframework.data.domain.Slice;
|
||||||
@@ -174,6 +177,69 @@ class AutoAssignCheckerIntTest extends AbstractJpaIntegrationTest {
|
|||||||
verifyThatTargetsNotHaveDistributionSetAssignment(toAssignDs, targets.subList(1, 25));
|
verifyThatTargetsNotHaveDistributionSetAssignment(toAssignDs, targets.subList(1, 25));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("confirmationOptions")
|
||||||
|
@Description("Test auto assignment of a DS to filtered targets with different confirmation options")
|
||||||
|
void checkAutoAssignWithConfirmationOptions(final boolean confirmationFlowActive, final boolean confirmationRequired,
|
||||||
|
final Action.Status expectedStatus) {
|
||||||
|
|
||||||
|
final DistributionSet distributionSet = testdataFactory.createDistributionSet("dsA");
|
||||||
|
|
||||||
|
if (confirmationFlowActive) {
|
||||||
|
enableConfirmationFlow();
|
||||||
|
}
|
||||||
|
|
||||||
|
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement.updateAutoAssignDS(entityFactory
|
||||||
|
.targetFilterQuery()
|
||||||
|
.updateAutoAssign(targetFilterQueryManagement
|
||||||
|
.create(entityFactory.targetFilterQuery().create().name("filterA").query("name==*")).getId())
|
||||||
|
.ds(distributionSet.getId()).confirmationRequired(confirmationRequired));
|
||||||
|
|
||||||
|
final String targetDsAIdPref = "targ";
|
||||||
|
final List<Target> targets = testdataFactory.createTargets(20, targetDsAIdPref,
|
||||||
|
targetDsAIdPref.concat(" description"));
|
||||||
|
|
||||||
|
// Run the check
|
||||||
|
autoAssignChecker.checkAllTargets();
|
||||||
|
|
||||||
|
verifyThatTargetsHaveDistributionSetAssignedAndActionStatus(distributionSet, targets, expectedStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("confirmationOptions")
|
||||||
|
@Description("Test auto assignment of a DS for a specific device with different confirmation options")
|
||||||
|
void checkAutoAssignmentForDeviceWithConfirmationRequired(final boolean confirmationFlowActive,
|
||||||
|
final boolean confirmationRequired, final Action.Status expectedStatus) {
|
||||||
|
|
||||||
|
final DistributionSet toAssignDs = testdataFactory.createDistributionSet();
|
||||||
|
|
||||||
|
if (confirmationFlowActive) {
|
||||||
|
enableConfirmationFlow();
|
||||||
|
}
|
||||||
|
|
||||||
|
// target filter query that matches all targets
|
||||||
|
targetFilterQueryManagement.updateAutoAssignDS(entityFactory.targetFilterQuery()
|
||||||
|
.updateAutoAssign(targetFilterQueryManagement
|
||||||
|
.create(entityFactory.targetFilterQuery().create().name("filterA").query("name==*")).getId())
|
||||||
|
.ds(toAssignDs.getId()).confirmationRequired(confirmationRequired));
|
||||||
|
|
||||||
|
final List<Target> targets = testdataFactory.createTargets(25);
|
||||||
|
|
||||||
|
// Run the check
|
||||||
|
autoAssignChecker.checkSingleTarget(targets.get(0).getControllerId());
|
||||||
|
verifyThatTargetsHaveDistributionSetAssignedAndActionStatus(toAssignDs, targets.subList(0, 1), expectedStatus);
|
||||||
|
|
||||||
|
verifyThatTargetsNotHaveDistributionSetAssignment(toAssignDs, targets.subList(1, 25));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> confirmationOptions() {
|
||||||
|
return Stream.of( //
|
||||||
|
Arguments.of(true, true, Status.WAIT_FOR_CONFIRMATION), //
|
||||||
|
Arguments.of(true, false, Status.RUNNING), //
|
||||||
|
Arguments.of(false, true, Status.RUNNING), //
|
||||||
|
Arguments.of(false, false, Status.RUNNING));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Description("Test auto assignment of an incomplete DS to filtered targets, that causes failures")
|
@Description("Test auto assignment of an incomplete DS to filtered targets, that causes failures")
|
||||||
void checkAutoAssignWithFailures() {
|
void checkAutoAssignWithFailures() {
|
||||||
@@ -246,6 +312,22 @@ class AutoAssignCheckerIntTest extends AbstractJpaIntegrationTest {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Step
|
||||||
|
private void verifyThatTargetsHaveDistributionSetAssignedAndActionStatus(final DistributionSet set,
|
||||||
|
final List<Target> targets, final Action.Status status) {
|
||||||
|
final List<String> targetIds = targets.stream().map(Target::getControllerId).collect(Collectors.toList());
|
||||||
|
final List<Target> targetsWithAssignedDS = targetManagement.findByAssignedDistributionSet(PAGE, set.getId())
|
||||||
|
.getContent();
|
||||||
|
assertThat(targetsWithAssignedDS).isNotEmpty();
|
||||||
|
assertThat(targetsWithAssignedDS).allMatch(target -> targetIds.contains(target.getControllerId()));
|
||||||
|
|
||||||
|
final List<Action> actionsByDs = deploymentManagement.findActionsByDistributionSet(PAGE, set.getId())
|
||||||
|
.getContent();
|
||||||
|
|
||||||
|
assertThat(actionsByDs).hasSize(targets.size());
|
||||||
|
assertThat(actionsByDs).allMatch(action -> action.getStatus() == status);
|
||||||
|
}
|
||||||
|
|
||||||
@Step
|
@Step
|
||||||
private void verifyThatTargetsNotHaveDistributionSetAssignment(final DistributionSet set,
|
private void verifyThatTargetsNotHaveDistributionSetAssignment(final DistributionSet set,
|
||||||
final List<Target> targets) {
|
final List<Target> targets) {
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ public class RSQLRolloutGroupFieldTest extends AbstractJpaIntegrationTest {
|
|||||||
return rolloutManagement.create(
|
return rolloutManagement.create(
|
||||||
entityFactory.rollout().create().set(distributionSetManagement.get(distributionSetId).get()).name(name)
|
entityFactory.rollout().create().set(distributionSetManagement.get(distributionSetId).get()).name(name)
|
||||||
.targetFilterQuery(targetFilterQuery),
|
.targetFilterQuery(targetFilterQuery),
|
||||||
amountGroups, new RolloutGroupConditionBuilder().withDefaults()
|
amountGroups, false, new RolloutGroupConditionBuilder().withDefaults()
|
||||||
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, "100").build());
|
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, "100").build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import org.apache.commons.lang3.RandomStringUtils;
|
|||||||
import org.eclipse.hawkbit.artifact.repository.ArtifactRepository;
|
import org.eclipse.hawkbit.artifact.repository.ArtifactRepository;
|
||||||
import org.eclipse.hawkbit.cache.TenantAwareCacheManager;
|
import org.eclipse.hawkbit.cache.TenantAwareCacheManager;
|
||||||
import org.eclipse.hawkbit.repository.ArtifactManagement;
|
import org.eclipse.hawkbit.repository.ArtifactManagement;
|
||||||
|
import org.eclipse.hawkbit.repository.ConfirmationManagement;
|
||||||
import org.eclipse.hawkbit.repository.ControllerManagement;
|
import org.eclipse.hawkbit.repository.ControllerManagement;
|
||||||
import org.eclipse.hawkbit.repository.DeploymentManagement;
|
import org.eclipse.hawkbit.repository.DeploymentManagement;
|
||||||
import org.eclipse.hawkbit.repository.DistributionSetInvalidationManagement;
|
import org.eclipse.hawkbit.repository.DistributionSetInvalidationManagement;
|
||||||
@@ -158,6 +159,8 @@ public abstract class AbstractIntegrationTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
protected DeploymentManagement deploymentManagement;
|
protected DeploymentManagement deploymentManagement;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
protected ConfirmationManagement confirmationManagement;
|
||||||
@Autowired
|
@Autowired
|
||||||
protected DistributionSetInvalidationManagement distributionSetInvalidationManagement;
|
protected DistributionSetInvalidationManagement distributionSetInvalidationManagement;
|
||||||
|
|
||||||
@@ -235,9 +238,12 @@ public abstract class AbstractIntegrationTest {
|
|||||||
|
|
||||||
protected DistributionSetAssignmentResult assignDistributionSet(final long dsID, final List<String> controllerIds,
|
protected DistributionSetAssignmentResult assignDistributionSet(final long dsID, final List<String> controllerIds,
|
||||||
final ActionType actionType, final long forcedTime, final Integer weight) {
|
final ActionType actionType, final long forcedTime, final Integer weight) {
|
||||||
|
final boolean confirmationFlowActive = isConfirmationFlowActive();
|
||||||
|
|
||||||
final List<DeploymentRequest> deploymentRequests = controllerIds.stream()
|
final List<DeploymentRequest> deploymentRequests = controllerIds.stream()
|
||||||
.map(id -> DeploymentManagement.deploymentRequest(id, dsID).setActionType(actionType)
|
.map(id -> DeploymentManagement.deploymentRequest(id, dsID).setActionType(actionType)
|
||||||
.setForceTime(forcedTime).setWeight(weight).build())
|
.setForceTime(forcedTime).setWeight(weight).setConfirmationRequired(confirmationFlowActive)
|
||||||
|
.build())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
final List<DistributionSetAssignmentResult> results = deploymentManagement
|
final List<DistributionSetAssignmentResult> results = deploymentManagement
|
||||||
.assignDistributionSets(deploymentRequests);
|
.assignDistributionSets(deploymentRequests);
|
||||||
@@ -245,6 +251,13 @@ public abstract class AbstractIntegrationTest {
|
|||||||
return results.get(0);
|
return results.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected List<DistributionSetAssignmentResult> assignDistributionSets(final List<DeploymentRequest> requests) {
|
||||||
|
final List<DistributionSetAssignmentResult> distributionSetAssignmentResults = deploymentManagement
|
||||||
|
.assignDistributionSets(requests);
|
||||||
|
assertThat(distributionSetAssignmentResults).hasSize(requests.size());
|
||||||
|
return distributionSetAssignmentResults;
|
||||||
|
}
|
||||||
|
|
||||||
protected DistributionSetAssignmentResult assignDistributionSet(final DistributionSet ds,
|
protected DistributionSetAssignmentResult assignDistributionSet(final DistributionSet ds,
|
||||||
final List<Target> targets) {
|
final List<Target> targets) {
|
||||||
final List<String> targetIds = targets.stream().map(Target::getControllerId).collect(Collectors.toList());
|
final List<String> targetIds = targets.stream().map(Target::getControllerId).collect(Collectors.toList());
|
||||||
@@ -296,7 +309,7 @@ public abstract class AbstractIntegrationTest {
|
|||||||
|
|
||||||
return makeAssignment(DeploymentManagement.deploymentRequest(controllerId, dsID)
|
return makeAssignment(DeploymentManagement.deploymentRequest(controllerId, dsID)
|
||||||
.setMaintenance(maintenanceWindowSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone)
|
.setMaintenance(maintenanceWindowSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone)
|
||||||
.build());
|
.setConfirmationRequired(true).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected DistributionSetAssignmentResult assignDistributionSet(final DistributionSet pset, final Target target) {
|
protected DistributionSetAssignmentResult assignDistributionSet(final DistributionSet pset, final Target target) {
|
||||||
@@ -312,6 +325,19 @@ public abstract class AbstractIntegrationTest {
|
|||||||
tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED, true);
|
tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void enableConfirmationFlow() {
|
||||||
|
tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.USER_CONFIRMATION_ENABLED, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void disableConfirmationFlow() {
|
||||||
|
tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.USER_CONFIRMATION_ENABLED, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isConfirmationFlowActive() {
|
||||||
|
return tenantConfigurationManagement.getConfigurationValue(TenantConfigurationKey.USER_CONFIRMATION_ENABLED,
|
||||||
|
Boolean.class).getValue();
|
||||||
|
}
|
||||||
|
|
||||||
protected DistributionSetMetadata createDistributionSetMetadata(final long dsId, final MetaData md) {
|
protected DistributionSetMetadata createDistributionSetMetadata(final long dsId, final MetaData md) {
|
||||||
return createDistributionSetMetadata(dsId, Collections.singletonList(md)).get(0);
|
return createDistributionSetMetadata(dsId, Collections.singletonList(md)).get(0);
|
||||||
}
|
}
|
||||||
@@ -345,6 +371,10 @@ public abstract class AbstractIntegrationTest {
|
|||||||
Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||||
.getContent().get(0);
|
.getContent().get(0);
|
||||||
|
|
||||||
|
if (savedAction.getStatus() == Action.Status.WAIT_FOR_CONFIRMATION) {
|
||||||
|
confirmationManagement.confirmAction(savedAction.getId(), null, null);
|
||||||
|
}
|
||||||
|
|
||||||
savedAction = controllerManagement.addUpdateActionStatus(
|
savedAction = controllerManagement.addUpdateActionStatus(
|
||||||
entityFactory.actionStatus().create(savedAction.getId()).status(Action.Status.RUNNING));
|
entityFactory.actionStatus().create(savedAction.getId()).status(Action.Status.RUNNING));
|
||||||
|
|
||||||
@@ -473,4 +503,10 @@ public abstract class AbstractIntegrationTest {
|
|||||||
protected void disableBatchAssignments() {
|
protected void disableBatchAssignments() {
|
||||||
tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.BATCH_ASSIGNMENTS_ENABLED, false);
|
tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.BATCH_ASSIGNMENTS_ENABLED, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected boolean isConfirmationFlowEnabled() {
|
||||||
|
return tenantConfigurationManagement
|
||||||
|
.getConfigurationValue(TenantConfigurationKey.USER_CONFIRMATION_ENABLED, Boolean.class).getValue();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -877,10 +877,14 @@ public class TestdataFactory {
|
|||||||
* @return {@link List} of {@link Target} entities
|
* @return {@link List} of {@link Target} entities
|
||||||
*/
|
*/
|
||||||
public List<Target> createTargets(final int number) {
|
public List<Target> createTargets(final int number) {
|
||||||
|
return createTargets(DEFAULT_CONTROLLER_ID, number);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Target> createTargets(final String prefix, final int number) {
|
||||||
|
|
||||||
final List<TargetCreate> targets = Lists.newArrayListWithExpectedSize(number);
|
final List<TargetCreate> targets = Lists.newArrayListWithExpectedSize(number);
|
||||||
for (int i = 0; i < number; i++) {
|
for (int i = 0; i < number; i++) {
|
||||||
targets.add(entityFactory.target().create().controllerId(DEFAULT_CONTROLLER_ID + i));
|
targets.add(entityFactory.target().create().controllerId(prefix + i));
|
||||||
}
|
}
|
||||||
|
|
||||||
return createTargets(targets);
|
return createTargets(targets);
|
||||||
@@ -1140,7 +1144,14 @@ public class TestdataFactory {
|
|||||||
final int groupSize, final String filterQuery, final DistributionSet distributionSet,
|
final int groupSize, final String filterQuery, final DistributionSet distributionSet,
|
||||||
final String successCondition, final String errorCondition) {
|
final String successCondition, final String errorCondition) {
|
||||||
return createRolloutByVariables(rolloutName, rolloutDescription, groupSize, filterQuery, distributionSet,
|
return createRolloutByVariables(rolloutName, rolloutDescription, groupSize, filterQuery, distributionSet,
|
||||||
successCondition, errorCondition, Action.ActionType.FORCED, null);
|
successCondition, errorCondition, Action.ActionType.FORCED, null, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Rollout createRolloutByVariables(final String rolloutName, final String rolloutDescription,
|
||||||
|
final int groupSize, final String filterQuery, final DistributionSet distributionSet,
|
||||||
|
final String successCondition, final String errorCondition, final boolean confirmationRequired) {
|
||||||
|
return createRolloutByVariables(rolloutName, rolloutDescription, groupSize, filterQuery, distributionSet,
|
||||||
|
successCondition, errorCondition, Action.ActionType.FORCED, null, confirmationRequired);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1164,12 +1175,15 @@ public class TestdataFactory {
|
|||||||
* the type of the Rollout
|
* the type of the Rollout
|
||||||
* @param weight
|
* @param weight
|
||||||
* weight of the Rollout
|
* weight of the Rollout
|
||||||
|
* @param confirmationRequired
|
||||||
|
* if the confirmation is required (considered with confirmation flow
|
||||||
|
* active)
|
||||||
* @return created {@link Rollout}
|
* @return created {@link Rollout}
|
||||||
*/
|
*/
|
||||||
public Rollout createRolloutByVariables(final String rolloutName, final String rolloutDescription,
|
public Rollout createRolloutByVariables(final String rolloutName, final String rolloutDescription,
|
||||||
final int groupSize, final String filterQuery, final DistributionSet distributionSet,
|
final int groupSize, final String filterQuery, final DistributionSet distributionSet,
|
||||||
final String successCondition, final String errorCondition, final Action.ActionType actionType,
|
final String successCondition, final String errorCondition, final Action.ActionType actionType,
|
||||||
final Integer weight) {
|
final Integer weight, final boolean confirmationRequired) {
|
||||||
final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder().withDefaults()
|
final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder().withDefaults()
|
||||||
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, successCondition)
|
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, successCondition)
|
||||||
.errorCondition(RolloutGroupErrorCondition.THRESHOLD, errorCondition)
|
.errorCondition(RolloutGroupErrorCondition.THRESHOLD, errorCondition)
|
||||||
@@ -1178,7 +1192,7 @@ public class TestdataFactory {
|
|||||||
final Rollout rollout = rolloutManagement.create(
|
final Rollout rollout = rolloutManagement.create(
|
||||||
entityFactory.rollout().create().name(rolloutName).description(rolloutDescription)
|
entityFactory.rollout().create().name(rolloutName).description(rolloutDescription)
|
||||||
.targetFilterQuery(filterQuery).set(distributionSet).actionType(actionType).weight(weight),
|
.targetFilterQuery(filterQuery).set(distributionSet).actionType(actionType).weight(weight),
|
||||||
groupSize, conditions);
|
groupSize, confirmationRequired, conditions);
|
||||||
|
|
||||||
// Run here, because Scheduler is disabled during tests
|
// Run here, because Scheduler is disabled during tests
|
||||||
rolloutManagement.handleRollouts();
|
rolloutManagement.handleRollouts();
|
||||||
@@ -1314,7 +1328,7 @@ public class TestdataFactory {
|
|||||||
createTargets(amountOtherTargets, "others-" + suffix + "-", "rollout");
|
createTargets(amountOtherTargets, "others-" + suffix + "-", "rollout");
|
||||||
final String filterQuery = "controllerId==rollout-" + suffix + "-*";
|
final String filterQuery = "controllerId==rollout-" + suffix + "-*";
|
||||||
return createRolloutByVariables("rollout-" + suffix, "test-rollout-description", amountOfGroups,
|
return createRolloutByVariables("rollout-" + suffix, "test-rollout-description", amountOfGroups,
|
||||||
filterQuery, rolloutDS, successCondition, errorCondition, actionType, weight);
|
filterQuery, rolloutDS, successCondition, errorCondition, actionType, weight, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Provide action history information to the controller as part of response to
|
* Provide action history information to the controller as part of response to
|
||||||
* {@link DdiRootControllerRestApi#getControllerBasedeploymentAction}: 1.
|
* {@link DdiRootControllerRestApi#getControllerBasedeploymentAction} and
|
||||||
* Current action status at the server; 2. List of messages from action history
|
* {@link DdiRootControllerRestApi#getControllerBaseconfirmationAction}:
|
||||||
|
* 1. Current action status at the server; 2. List of messages from action history
|
||||||
* that were sent to server earlier by the controller using
|
* that were sent to server earlier by the controller using
|
||||||
* {@link DdiActionFeedback}.
|
* {@link DdiActionFeedback}.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2022 Bosch.IO 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.ddi.json.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class DdiActivateAutoConfirmation {
|
||||||
|
|
||||||
|
@JsonProperty(required = false)
|
||||||
|
private final String initiator;
|
||||||
|
|
||||||
|
@JsonProperty(required = false)
|
||||||
|
private final String remark;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param initiator
|
||||||
|
* can be null
|
||||||
|
* @param remark
|
||||||
|
* can be null
|
||||||
|
*/
|
||||||
|
@JsonCreator
|
||||||
|
public DdiActivateAutoConfirmation(@JsonProperty(value = "initiator") final String initiator,
|
||||||
|
@JsonProperty(value = "remark") final String remark) {
|
||||||
|
this.initiator = initiator;
|
||||||
|
this.remark = remark;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "DdiActivateAutoConfirmation [initiator=" + initiator + ", remark=" + remark + ", toString()="
|
||||||
|
+ super.toString() + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getInitiator() {
|
||||||
|
return initiator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRemark() {
|
||||||
|
return remark;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2022 Bosch.IO 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.ddi.json.model;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||||
|
import org.springframework.hateoas.RepresentationModel;
|
||||||
|
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
@JsonPropertyOrder({ "active", "initiator", "remark", "activatedAt" })
|
||||||
|
public class DdiAutoConfirmationState extends RepresentationModel<DdiAutoConfirmationState> {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private boolean active;
|
||||||
|
private String initiator;
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
private Long activatedAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*/
|
||||||
|
public DdiAutoConfirmationState() {
|
||||||
|
// needed for json create.
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DdiAutoConfirmationState active(final long activatedAt) {
|
||||||
|
final DdiAutoConfirmationState state = new DdiAutoConfirmationState();
|
||||||
|
state.setActive(true);
|
||||||
|
state.setActivatedAt(activatedAt);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DdiAutoConfirmationState disabled() {
|
||||||
|
return new DdiAutoConfirmationState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isActive() {
|
||||||
|
return active;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActive(final boolean active) {
|
||||||
|
this.active = active;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getActivatedAt() {
|
||||||
|
return activatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActivatedAt(final long activatedAt) {
|
||||||
|
this.activatedAt = activatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getInitiator() {
|
||||||
|
return initiator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInitiator(final String initiator) {
|
||||||
|
this.initiator = initiator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRemark() {
|
||||||
|
return remark;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRemark(final String remark) {
|
||||||
|
this.remark = remark;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2022 Bosch.IO 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.ddi.json.model;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||||
|
import org.springframework.hateoas.RepresentationModel;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirmation base response.
|
||||||
|
* Set order to place links at last.
|
||||||
|
*/
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
@JsonPropertyOrder({ "autoConfirm" })
|
||||||
|
public class DdiConfirmationBase extends RepresentationModel<DdiConfirmationBase> {
|
||||||
|
|
||||||
|
@JsonProperty("autoConfirm")
|
||||||
|
@NotNull
|
||||||
|
private DdiAutoConfirmationState autoConfirm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*/
|
||||||
|
public DdiConfirmationBase() {
|
||||||
|
// needed for json create.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param autoConfirmState
|
||||||
|
*/
|
||||||
|
public DdiConfirmationBase(final DdiAutoConfirmationState autoConfirmState) {
|
||||||
|
this.autoConfirm = autoConfirmState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DdiAutoConfirmationState getAutoConfirm() {
|
||||||
|
return autoConfirm;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2022 Bosch.IO 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.ddi.json.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||||
|
import org.springframework.hateoas.RepresentationModel;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update action resource.
|
||||||
|
*/
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
@JsonPropertyOrder({ "id", "confirmation", "actionHistory" })
|
||||||
|
public class DdiConfirmationBaseAction extends RepresentationModel<DdiConfirmationBaseAction> {
|
||||||
|
|
||||||
|
@JsonProperty("id")
|
||||||
|
@NotNull
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@JsonProperty("confirmation")
|
||||||
|
@NotNull
|
||||||
|
private DdiDeployment confirmation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action history containing current action status and a list of feedback
|
||||||
|
* messages received earlier from the controller.
|
||||||
|
*/
|
||||||
|
@JsonProperty("actionHistory")
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
private DdiActionHistory actionHistory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*/
|
||||||
|
public DdiConfirmationBaseAction() {
|
||||||
|
// needed for json create.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
* of the update action
|
||||||
|
* @param confirmation
|
||||||
|
* chunk details
|
||||||
|
* @param actionHistory
|
||||||
|
* containing current action status and a list of feedback messages
|
||||||
|
* received earlier from the controller.
|
||||||
|
*/
|
||||||
|
public DdiConfirmationBaseAction(final String id, final DdiDeployment confirmation,
|
||||||
|
final DdiActionHistory actionHistory) {
|
||||||
|
this.id = id;
|
||||||
|
this.confirmation = confirmation;
|
||||||
|
this.actionHistory = actionHistory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the confirmation data containing chunks needed to confirm a action.
|
||||||
|
*
|
||||||
|
* @return {@link DdiDeployment}
|
||||||
|
*/
|
||||||
|
public DdiDeployment getConfirmation() {
|
||||||
|
return confirmation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the action history containing current action status and a list of
|
||||||
|
* feedback messages received earlier from the controller.
|
||||||
|
*
|
||||||
|
* @return {@link DdiActionHistory}
|
||||||
|
*/
|
||||||
|
public DdiActionHistory getActionHistory() {
|
||||||
|
return actionHistory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ConfirmationBase [id=" + id + ", confirmation=" + confirmation + " actionHistory=" + actionHistory
|
||||||
|
+ "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (obj == this) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final DdiConfirmationBaseAction oBase = (DdiConfirmationBaseAction) obj;
|
||||||
|
return Objects.equals(this.id, oBase.id) && Objects.equals(this.confirmation, oBase.confirmation)
|
||||||
|
&& Objects.equals(this.actionHistory, oBase.actionHistory);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2022 Bosch.IO 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.ddi.json.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonValue;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* New update actions require confirmation when confirmation flow is switched on. This is the feedback channel for
|
||||||
|
* confirmation messages for DDI API. The confirmation message has a mandatory field confirmation with possible values:
|
||||||
|
* "confirmed" and "denied".
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class DdiConfirmationFeedback {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
private final Confirmation confirmation;
|
||||||
|
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
private final List<String> details;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an confirmation-feedback
|
||||||
|
*
|
||||||
|
* @param confirmation confirmation value for the action. Valid values are "Confirmed" and "Denied
|
||||||
|
* @param code code for confirmation
|
||||||
|
* @param details messages
|
||||||
|
*/
|
||||||
|
@JsonCreator public DdiConfirmationFeedback(
|
||||||
|
@JsonProperty(value = "confirmation", required = true) final Confirmation confirmation,
|
||||||
|
@JsonProperty(value = "code") final Integer code,
|
||||||
|
@JsonProperty(value = "details") final List<String> details) {
|
||||||
|
this.confirmation = confirmation;
|
||||||
|
this.code = code;
|
||||||
|
this.details = details;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Confirmation getConfirmation() {
|
||||||
|
return confirmation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getDetails() {
|
||||||
|
if (details == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableList(details);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Confirmation {
|
||||||
|
/**
|
||||||
|
* Confirm the action.
|
||||||
|
*/
|
||||||
|
CONFIRMED("confirmed"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deny the action.
|
||||||
|
*/
|
||||||
|
DENIED("denied");
|
||||||
|
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
Confirmation(final String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonValue
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "DdiConfirmationFeedback [confirmation=" + confirmation + ", code=" + code + ", details=" + details +"]";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,21 @@ public final class DdiRestConstants {
|
|||||||
*/
|
*/
|
||||||
public static final String DEPLOYMENT_BASE_ACTION = "deploymentBase";
|
public static final String DEPLOYMENT_BASE_ACTION = "deploymentBase";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirmation base resource.
|
||||||
|
*/
|
||||||
|
public static final String CONFIRMATION_BASE = "confirmationBase";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate auto-confirm
|
||||||
|
*/
|
||||||
|
public static final String AUTO_CONFIRM_ACTIVATE = "activateAutoConfirm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deactivate auto-confirm
|
||||||
|
*/
|
||||||
|
public static final String AUTO_CONFIRM_DEACTIVATE = "deactivateAutoConfirm";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Installed action resources.
|
* Installed action resources.
|
||||||
*/
|
*/
|
||||||
@@ -52,6 +67,7 @@ public final class DdiRestConstants {
|
|||||||
* Default value specifying that no action history to be sent as part of
|
* Default value specifying that no action history to be sent as part of
|
||||||
* response to deploymentBase
|
* response to deploymentBase
|
||||||
* {@link DdiRootControllerRestApi#getControllerBasedeploymentAction}.
|
* {@link DdiRootControllerRestApi#getControllerBasedeploymentAction}.
|
||||||
|
* {@link DdiRootControllerRestApi#getConfirmationBaseAction}.
|
||||||
*/
|
*/
|
||||||
public static final String NO_ACTION_HISTORY = "0";
|
public static final String NO_ACTION_HISTORY = "0";
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,14 @@ import javax.validation.Valid;
|
|||||||
import javax.validation.constraints.NotEmpty;
|
import javax.validation.constraints.NotEmpty;
|
||||||
|
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiActionFeedback;
|
import org.eclipse.hawkbit.ddi.json.model.DdiActionFeedback;
|
||||||
|
import org.eclipse.hawkbit.ddi.json.model.DdiActivateAutoConfirmation;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiArtifact;
|
import org.eclipse.hawkbit.ddi.json.model.DdiArtifact;
|
||||||
|
import org.eclipse.hawkbit.ddi.json.model.DdiAutoConfirmationState;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiCancel;
|
import org.eclipse.hawkbit.ddi.json.model.DdiCancel;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiConfigData;
|
import org.eclipse.hawkbit.ddi.json.model.DdiConfigData;
|
||||||
|
import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationBase;
|
||||||
|
import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationBaseAction;
|
||||||
|
import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationFeedback;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiControllerBase;
|
import org.eclipse.hawkbit.ddi.json.model.DdiControllerBase;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiDeploymentBase;
|
import org.eclipse.hawkbit.ddi.json.model.DdiDeploymentBase;
|
||||||
import org.springframework.hateoas.MediaTypes;
|
import org.springframework.hateoas.MediaTypes;
|
||||||
@@ -29,6 +34,7 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.PutMapping;
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
@@ -123,17 +129,16 @@ public interface DdiRootControllerRestApi {
|
|||||||
* @param controllerId
|
* @param controllerId
|
||||||
* of the target
|
* of the target
|
||||||
* @param actionId
|
* @param actionId
|
||||||
* of the {@link DdiDeploymentBase} that matches to active
|
* of the {@link DdiDeploymentBase} that matches to active actions.
|
||||||
* actions.
|
|
||||||
* @param resource
|
* @param resource
|
||||||
* an hashcode of the resource which indicates if the action has
|
* an hashcode of the resource which indicates if the action has been
|
||||||
* been changed, e.g. from 'soft' to 'force' and the eTag needs
|
* changed, e.g. from 'soft' to 'force' and the eTag needs to be
|
||||||
* to be re-generated
|
* re-generated
|
||||||
* @param actionHistoryMessageCount
|
* @param actionHistoryMessageCount
|
||||||
* specifies the number of messages to be returned from action
|
* specifies the number of messages to be returned from action
|
||||||
* history. Regardless of the passed value, in order to restrict
|
* history. Regardless of the passed value, in order to restrict
|
||||||
* resource utilization by controllers, maximum number of
|
* resource utilization by controllers, maximum number of messages
|
||||||
* messages that are retrieved from database is limited by
|
* that are retrieved from database is limited by
|
||||||
* {@link RepositoryConstants#MAX_ACTION_HISTORY_MSG_COUNT}.
|
* {@link RepositoryConstants#MAX_ACTION_HISTORY_MSG_COUNT}.
|
||||||
*
|
*
|
||||||
* actionHistoryMessageCount less than zero: retrieves the maximum
|
* actionHistoryMessageCount less than zero: retrieves the maximum
|
||||||
@@ -212,8 +217,8 @@ public interface DdiRootControllerRestApi {
|
|||||||
@PathVariable("actionId") @NotEmpty final Long actionId);
|
@PathVariable("actionId") @NotEmpty final Long actionId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RequestMethod.POST method receiving the {@link DdiActionFeedback} from
|
* RequestMethod.POST method receiving the {@link DdiActionFeedback} from the
|
||||||
* the target.
|
* target.
|
||||||
*
|
*
|
||||||
* @param feedback
|
* @param feedback
|
||||||
* the {@link DdiActionFeedback} from the target.
|
* the {@link DdiActionFeedback} from the target.
|
||||||
@@ -270,4 +275,119 @@ public interface DdiRootControllerRestApi {
|
|||||||
@PathVariable("actionId") @NotEmpty final Long actionId,
|
@PathVariable("actionId") @NotEmpty final Long actionId,
|
||||||
@RequestParam(value = "actionHistory", defaultValue = DdiRestConstants.NO_ACTION_HISTORY) final Integer actionHistoryMessageCount);
|
@RequestParam(value = "actionHistory", defaultValue = DdiRestConstants.NO_ACTION_HISTORY) final Integer actionHistoryMessageCount);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the confirmation base with the current auto-confirmation state for a
|
||||||
|
* given controllerId and toggle links. In case there are actions present where
|
||||||
|
* the confirmation is required, a reference to it will be returned as well.
|
||||||
|
*
|
||||||
|
* @param tenant
|
||||||
|
* the controllerId is corresponding too
|
||||||
|
* @param controllerId
|
||||||
|
* to check the state for
|
||||||
|
* @return the state as {@link DdiAutoConfirmationState}
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/{controllerId}/" + DdiRestConstants.CONFIRMATION_BASE, produces = {
|
||||||
|
MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
|
||||||
|
ResponseEntity<DdiConfirmationBase> getConfirmationBase(@PathVariable("tenant") final String tenant,
|
||||||
|
@PathVariable("controllerId") @NotEmpty final String controllerId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resource for confirmation of an action.
|
||||||
|
*
|
||||||
|
* @param tenant
|
||||||
|
* of the request
|
||||||
|
* @param controllerId
|
||||||
|
* of the target
|
||||||
|
* @param actionId
|
||||||
|
* of the {@link DdiConfirmationBaseAction} that matches to active
|
||||||
|
* actions in WAITING_FOR_CONFIRMATION status.
|
||||||
|
* @param resource
|
||||||
|
* an hashcode of the resource which indicates if the action has been
|
||||||
|
* changed, e.g. from 'soft' to 'force' and the eTag needs to be
|
||||||
|
* re-generated
|
||||||
|
* @param actionHistoryMessageCount
|
||||||
|
* specifies the number of messages to be returned from action
|
||||||
|
* history. Regardless of the passed value, in order to restrict
|
||||||
|
* resource utilization by controllers, maximum number of messages
|
||||||
|
* that are retrieved from database is limited by
|
||||||
|
* {@link RepositoryConstants#MAX_ACTION_HISTORY_MSG_COUNT}.
|
||||||
|
*
|
||||||
|
* actionHistoryMessageCount less than zero: retrieves the maximum
|
||||||
|
* allowed number of action status messages from history;
|
||||||
|
*
|
||||||
|
* actionHistoryMessageCount equal to zero: does not retrieve any
|
||||||
|
* message;
|
||||||
|
*
|
||||||
|
* actionHistoryMessageCount greater than zero: retrieves the
|
||||||
|
* specified number of messages, limited by maximum allowed number.
|
||||||
|
*
|
||||||
|
* @return the response
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/{controllerId}/" + DdiRestConstants.CONFIRMATION_BASE + "/{actionId}", produces = {
|
||||||
|
MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
|
||||||
|
ResponseEntity<DdiConfirmationBaseAction> getConfirmationBaseAction(@PathVariable("tenant") final String tenant,
|
||||||
|
@PathVariable("controllerId") @NotEmpty final String controllerId,
|
||||||
|
@PathVariable("actionId") @NotEmpty final Long actionId,
|
||||||
|
@RequestParam(value = "c", required = false, defaultValue = "-1") final int resource,
|
||||||
|
@RequestParam(value = "actionHistory", defaultValue = DdiRestConstants.NO_ACTION_HISTORY) final Integer actionHistoryMessageCount);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the feedback channel for the {@link DdiConfirmationBaseAction}
|
||||||
|
* action.
|
||||||
|
*
|
||||||
|
* @param tenant
|
||||||
|
* of the client
|
||||||
|
* @param feedback
|
||||||
|
* to provide
|
||||||
|
* @param controllerId
|
||||||
|
* of the target that matches to controller id
|
||||||
|
* @param actionId
|
||||||
|
* of the action we have feedback for
|
||||||
|
*
|
||||||
|
* @return the response
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/{controllerId}/" + DdiRestConstants.CONFIRMATION_BASE + "/{actionId}/"
|
||||||
|
+ DdiRestConstants.FEEDBACK, consumes = { MediaType.APPLICATION_JSON_VALUE,
|
||||||
|
DdiRestConstants.MEDIA_TYPE_CBOR })
|
||||||
|
ResponseEntity<Void> postConfirmationActionFeedback(@Valid final DdiConfirmationFeedback feedback,
|
||||||
|
@PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId,
|
||||||
|
@PathVariable("actionId") @NotEmpty final Long actionId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate auto confirmation for a given controllerId. Will use the provided
|
||||||
|
* initiator and remark field from the provided
|
||||||
|
* {@link DdiActivateAutoConfirmation}. If not present, the values will be
|
||||||
|
* prefilled with a default remark and the CONTROLLER as initiator.
|
||||||
|
*
|
||||||
|
* @param tenant
|
||||||
|
* the controllerId is corresponding too
|
||||||
|
* @param controllerId
|
||||||
|
* to activate auto-confirmation for
|
||||||
|
* @param body
|
||||||
|
* as {@link DdiActivateAutoConfirmation}
|
||||||
|
* @return {@link org.springframework.http.HttpStatus#OK} if successful or
|
||||||
|
* {@link org.springframework.http.HttpStatus#CONFLICT} in case
|
||||||
|
* auto-confirmation was active already.
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/{controllerId}/" + DdiRestConstants.CONFIRMATION_BASE + "/"
|
||||||
|
+ DdiRestConstants.AUTO_CONFIRM_ACTIVATE, consumes = { MediaType.APPLICATION_JSON_VALUE,
|
||||||
|
DdiRestConstants.MEDIA_TYPE_CBOR })
|
||||||
|
ResponseEntity<Void> activateAutoConfirmation(@PathVariable("tenant") final String tenant,
|
||||||
|
@PathVariable("controllerId") @NotEmpty final String controllerId,
|
||||||
|
@Valid @RequestBody(required = false) final DdiActivateAutoConfirmation body);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deactivate auto confirmation for a given controller id.
|
||||||
|
*
|
||||||
|
* @param tenant
|
||||||
|
* the controllerId is corresponding too
|
||||||
|
* @param controllerId
|
||||||
|
* to disable auto-confirmation for
|
||||||
|
* @return {@link org.springframework.http.HttpStatus#OK} if successfully
|
||||||
|
* executed
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/{controllerId}/" + DdiRestConstants.CONFIRMATION_BASE + "/"
|
||||||
|
+ DdiRestConstants.AUTO_CONFIRM_DEACTIVATE)
|
||||||
|
ResponseEntity<Void> deactivateAutoConfirmation(@PathVariable("tenant") final String tenant,
|
||||||
|
@PathVariable("controllerId") @NotEmpty final String controllerId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2022 Bosch.IO 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.ddi.json.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
|
||||||
|
import io.qameta.allure.Description;
|
||||||
|
import io.qameta.allure.Feature;
|
||||||
|
import io.qameta.allure.Story;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
import static org.eclipse.hawkbit.ddi.json.model.DdiDeployment.DdiMaintenanceWindowStatus.AVAILABLE;
|
||||||
|
import static org.eclipse.hawkbit.ddi.json.model.DdiDeployment.HandlingType.ATTEMPT;
|
||||||
|
import static org.eclipse.hawkbit.ddi.json.model.DdiDeployment.HandlingType.FORCED;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test serializability of DDI api model 'DdiConfirmationBase'
|
||||||
|
*/
|
||||||
|
@Feature("Unit Tests - Direct Device Integration API")
|
||||||
|
@Story("CHeck JSON serialization of DDI api confirmation models")
|
||||||
|
class DdiConfirmationBaseTest {
|
||||||
|
|
||||||
|
private final ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Verify the correct serialization and deserialization of the model")
|
||||||
|
void shouldSerializeAndDeserializeObject() throws IOException {
|
||||||
|
// Setup
|
||||||
|
final String id = "1234";
|
||||||
|
final DdiDeployment ddiDeployment = new DdiDeployment(FORCED, ATTEMPT, Collections.emptyList(), AVAILABLE);
|
||||||
|
final String actionStatus = "TestAction";
|
||||||
|
final DdiActionHistory ddiActionHistory = new DdiActionHistory(actionStatus,
|
||||||
|
Arrays.asList("Action status message 1", "Action status message 2"));
|
||||||
|
final DdiConfirmationBaseAction ddiConfirmationBaseAction = new DdiConfirmationBaseAction(id, ddiDeployment,
|
||||||
|
ddiActionHistory);
|
||||||
|
|
||||||
|
// Test
|
||||||
|
String serializedDdiConfirmationBase = mapper.writeValueAsString(ddiConfirmationBaseAction);
|
||||||
|
final DdiConfirmationBaseAction deserializedDdiConfigurationBase = mapper
|
||||||
|
.readValue(serializedDdiConfirmationBase, DdiConfirmationBaseAction.class);
|
||||||
|
|
||||||
|
assertThat(serializedDdiConfirmationBase).contains(id, FORCED.getName(), ATTEMPT.getName(),
|
||||||
|
AVAILABLE.getStatus(), actionStatus);
|
||||||
|
assertThat(deserializedDdiConfigurationBase.getConfirmation().getDownload())
|
||||||
|
.isEqualTo(ddiDeployment.getDownload());
|
||||||
|
assertThat(deserializedDdiConfigurationBase.getConfirmation().getUpdate()).isEqualTo(ddiDeployment.getUpdate());
|
||||||
|
assertThat(deserializedDdiConfigurationBase.getConfirmation().getMaintenanceWindow())
|
||||||
|
.isEqualTo(ddiDeployment.getMaintenanceWindow());
|
||||||
|
assertThat(deserializedDdiConfigurationBase.getActionHistory().toString())
|
||||||
|
.isEqualTo(ddiActionHistory.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Verify the correct deserialization of a model with a additional unknown property")
|
||||||
|
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||||
|
// Setup
|
||||||
|
String serializedDdiConfirmationBase = "{\"id\":\"1234\",\"confirmation\":{\"download\":\"forced\","
|
||||||
|
+ "\"update\":\"attempt\",\"maintenanceWindow\":\"available\",\"chunks\":[]},"
|
||||||
|
+ "\"actionHistory\":{\"status\":\"TestAction\",\"messages\":[\"Action status message 1\","
|
||||||
|
+ "\"Action status message 2\"]},\"links\":[],\"unknownProperty\":\"test\"}";
|
||||||
|
|
||||||
|
// Test
|
||||||
|
DdiConfirmationBaseAction ddiConfirmationBaseAction = mapper.readValue(serializedDdiConfirmationBase,
|
||||||
|
DdiConfirmationBaseAction.class);
|
||||||
|
|
||||||
|
assertThat(ddiConfirmationBaseAction.getConfirmation().getDownload().getName()).isEqualTo(FORCED.getName());
|
||||||
|
assertThat(ddiConfirmationBaseAction.getConfirmation().getUpdate().getName()).isEqualTo(ATTEMPT.getName());
|
||||||
|
assertThat(ddiConfirmationBaseAction.getConfirmation().getMaintenanceWindow().getStatus())
|
||||||
|
.isEqualTo(AVAILABLE.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Verify that deserialization fails for known properties with a wrong datatype")
|
||||||
|
void shouldFailForObjectWithWrongDataTypes() throws IOException {
|
||||||
|
// Setup
|
||||||
|
String serializedDdiConfirmationBase = "{\"id\":[\"1234\"],\"confirmation\":{\"download\":\"forced\","
|
||||||
|
+ "\"update\":\"attempt\",\"maintenanceWindow\":\"available\",\"chunks\":[]},"
|
||||||
|
+ "\"actionHistory\":{\"status\":\"TestAction\",\"messages\":[\"Action status message 1\","
|
||||||
|
+ "\"Action status message 2\"]},\"links\":[]}";
|
||||||
|
|
||||||
|
// Test
|
||||||
|
assertThatExceptionOfType(MismatchedInputException.class)
|
||||||
|
.isThrownBy(() -> mapper.readValue(serializedDdiConfirmationBase, DdiConfirmationBaseAction.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,20 +31,23 @@ import io.qameta.allure.Story;
|
|||||||
public class JsonIgnorePropertiesAnnotationTest {
|
public class JsonIgnorePropertiesAnnotationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Description(
|
@Description("This test verifies that all model classes within the 'org.eclipse.hawkbit.ddi.json.model' package are annotated with '@JsonIgnoreProperties(ignoreUnknown = true)'")
|
||||||
"This test verifies that all model classes within the 'org.eclipse.hawkbit.ddi.json.model' package are annotated with '@JsonIgnoreProperties(ignoreUnknown = true)'")
|
|
||||||
public void shouldCheckAnnotationsForAllModelClasses() throws IOException {
|
public void shouldCheckAnnotationsForAllModelClasses() throws IOException {
|
||||||
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
|
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
|
||||||
final String packageName = this.getClass().getPackage().getName();
|
final String packageName = this.getClass().getPackage().getName();
|
||||||
|
|
||||||
final ImmutableSet<ClassPath.ClassInfo> topLevelClasses = ClassPath.from(loader).getTopLevelClasses(packageName);
|
final ImmutableSet<ClassPath.ClassInfo> topLevelClasses = ClassPath.from(loader)
|
||||||
|
.getTopLevelClasses(packageName);
|
||||||
for (final ClassPath.ClassInfo classInfo : topLevelClasses) {
|
for (final ClassPath.ClassInfo classInfo : topLevelClasses) {
|
||||||
final Class<?> modelClass = classInfo.load();
|
final Class<?> modelClass = classInfo.load();
|
||||||
if (modelClass.getSimpleName().contains("Test") || modelClass.isEnum()) {
|
if (modelClass.getSimpleName().contains("Test") || modelClass.isEnum()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final JsonIgnoreProperties annotation = modelClass.getAnnotation(JsonIgnoreProperties.class);
|
final JsonIgnoreProperties annotation = modelClass.getAnnotation(JsonIgnoreProperties.class);
|
||||||
assertThat(annotation).isNotNull();
|
assertThat(annotation)
|
||||||
|
.describedAs(
|
||||||
|
"Annotation 'JsonIgnoreProperties' is missing for class: " + modelClass.getSimpleName())
|
||||||
|
.isNotNull();
|
||||||
assertThat(annotation.ignoreUnknown()).isTrue();
|
assertThat(annotation.ignoreUnknown()).isTrue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ import org.eclipse.hawkbit.api.URLPlaceholder;
|
|||||||
import org.eclipse.hawkbit.api.URLPlaceholder.SoftwareData;
|
import org.eclipse.hawkbit.api.URLPlaceholder.SoftwareData;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiArtifact;
|
import org.eclipse.hawkbit.ddi.json.model.DdiArtifact;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiArtifactHash;
|
import org.eclipse.hawkbit.ddi.json.model.DdiArtifactHash;
|
||||||
|
import org.eclipse.hawkbit.ddi.json.model.DdiAutoConfirmationState;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiChunk;
|
import org.eclipse.hawkbit.ddi.json.model.DdiChunk;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiConfig;
|
import org.eclipse.hawkbit.ddi.json.model.DdiConfig;
|
||||||
|
import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationBase;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiControllerBase;
|
import org.eclipse.hawkbit.ddi.json.model.DdiControllerBase;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiMetadata;
|
import org.eclipse.hawkbit.ddi.json.model.DdiMetadata;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiPolling;
|
import org.eclipse.hawkbit.ddi.json.model.DdiPolling;
|
||||||
@@ -108,13 +110,46 @@ public final class DataConversionHelper {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static DdiConfirmationBase createConfirmationBase(final Target target, final Action activeAction,
|
||||||
|
final DdiAutoConfirmationState autoConfirmationState, final TenantAware tenantAware) {
|
||||||
|
final String controllerId = target.getControllerId();
|
||||||
|
final DdiConfirmationBase confirmationBase = new DdiConfirmationBase(autoConfirmationState);
|
||||||
|
if (autoConfirmationState.isActive()) {
|
||||||
|
confirmationBase.add(WebMvcLinkBuilder
|
||||||
|
.linkTo(WebMvcLinkBuilder.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
||||||
|
.deactivateAutoConfirmation(tenantAware.getCurrentTenant(), controllerId))
|
||||||
|
.withRel(DdiRestConstants.AUTO_CONFIRM_DEACTIVATE));
|
||||||
|
} else {
|
||||||
|
confirmationBase.add(WebMvcLinkBuilder
|
||||||
|
.linkTo(WebMvcLinkBuilder.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
||||||
|
.activateAutoConfirmation(tenantAware.getCurrentTenant(), controllerId, null))
|
||||||
|
.withRel(DdiRestConstants.AUTO_CONFIRM_ACTIVATE));
|
||||||
|
}
|
||||||
|
if (activeAction != null && activeAction.isWaitingConfirmation()) {
|
||||||
|
confirmationBase.add(WebMvcLinkBuilder
|
||||||
|
.linkTo(WebMvcLinkBuilder.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
||||||
|
.getConfirmationBaseAction(tenantAware.getCurrentTenant(), controllerId,
|
||||||
|
activeAction.getId(), calculateEtag(activeAction), null))
|
||||||
|
.withRel(DdiRestConstants.CONFIRMATION_BASE));
|
||||||
|
}
|
||||||
|
|
||||||
|
return confirmationBase;
|
||||||
|
}
|
||||||
|
|
||||||
public static DdiControllerBase fromTarget(final Target target, final Action installedAction,
|
public static DdiControllerBase fromTarget(final Target target, final Action installedAction,
|
||||||
final Action activeAction, final String defaultControllerPollTime, final TenantAware tenantAware) {
|
final Action activeAction, final String defaultControllerPollTime, final TenantAware tenantAware) {
|
||||||
final DdiControllerBase result = new DdiControllerBase(
|
final DdiControllerBase result = new DdiControllerBase(
|
||||||
new DdiConfig(new DdiPolling(defaultControllerPollTime)));
|
new DdiConfig(new DdiPolling(defaultControllerPollTime)));
|
||||||
|
|
||||||
if (activeAction != null) {
|
if (activeAction != null) {
|
||||||
if (activeAction.isCancelingOrCanceled()) {
|
if (activeAction.isWaitingConfirmation()) {
|
||||||
|
result.add(WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder
|
||||||
|
.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
||||||
|
.getConfirmationBaseAction(tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||||
|
activeAction.getId(), calculateEtag(activeAction), null))
|
||||||
|
.withRel(DdiRestConstants.CONFIRMATION_BASE));
|
||||||
|
|
||||||
|
} else if (activeAction.isCancelingOrCanceled()) {
|
||||||
result.add(WebMvcLinkBuilder
|
result.add(WebMvcLinkBuilder
|
||||||
.linkTo(WebMvcLinkBuilder.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
.linkTo(WebMvcLinkBuilder.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
||||||
.getControllerCancelAction(tenantAware.getCurrentTenant(), target.getControllerId(),
|
.getControllerCancelAction(tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||||
@@ -126,10 +161,10 @@ public final class DataConversionHelper {
|
|||||||
// have changed from 'soft' to 'forced' type and we need to
|
// have changed from 'soft' to 'forced' type and we need to
|
||||||
// change the payload of the
|
// change the payload of the
|
||||||
// response because of eTags.
|
// response because of eTags.
|
||||||
result.add(WebMvcLinkBuilder
|
result.add(WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder
|
||||||
.linkTo(WebMvcLinkBuilder.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
||||||
.getControllerBasedeploymentAction(tenantAware.getCurrentTenant(),
|
.getControllerBasedeploymentAction(tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||||
target.getControllerId(), activeAction.getId(), calculateEtag(activeAction), null))
|
activeAction.getId(), calculateEtag(activeAction), null))
|
||||||
.withRel(DdiRestConstants.DEPLOYMENT_BASE_ACTION));
|
.withRel(DdiRestConstants.DEPLOYMENT_BASE_ACTION));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,6 +184,7 @@ public final class DataConversionHelper {
|
|||||||
.putConfigData(null, tenantAware.getCurrentTenant(), target.getControllerId()))
|
.putConfigData(null, tenantAware.getCurrentTenant(), target.getControllerId()))
|
||||||
.withRel(DdiRestConstants.CONFIG_DATA_ACTION));
|
.withRel(DdiRestConstants.CONFIG_DATA_ACTION));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import java.io.InputStream;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
@@ -22,11 +23,16 @@ import org.eclipse.hawkbit.api.ArtifactUrlHandler;
|
|||||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiActionFeedback;
|
import org.eclipse.hawkbit.ddi.json.model.DdiActionFeedback;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiActionHistory;
|
import org.eclipse.hawkbit.ddi.json.model.DdiActionHistory;
|
||||||
|
import org.eclipse.hawkbit.ddi.json.model.DdiActivateAutoConfirmation;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiArtifact;
|
import org.eclipse.hawkbit.ddi.json.model.DdiArtifact;
|
||||||
|
import org.eclipse.hawkbit.ddi.json.model.DdiAutoConfirmationState;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiCancel;
|
import org.eclipse.hawkbit.ddi.json.model.DdiCancel;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiCancelActionToStop;
|
import org.eclipse.hawkbit.ddi.json.model.DdiCancelActionToStop;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiChunk;
|
import org.eclipse.hawkbit.ddi.json.model.DdiChunk;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiConfigData;
|
import org.eclipse.hawkbit.ddi.json.model.DdiConfigData;
|
||||||
|
import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationBase;
|
||||||
|
import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationBaseAction;
|
||||||
|
import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationFeedback;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiControllerBase;
|
import org.eclipse.hawkbit.ddi.json.model.DdiControllerBase;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiDeployment;
|
import org.eclipse.hawkbit.ddi.json.model.DdiDeployment;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiDeployment.DdiMaintenanceWindowStatus;
|
import org.eclipse.hawkbit.ddi.json.model.DdiDeployment.DdiMaintenanceWindowStatus;
|
||||||
@@ -37,6 +43,7 @@ import org.eclipse.hawkbit.ddi.json.model.DdiUpdateMode;
|
|||||||
import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
|
import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
|
||||||
import org.eclipse.hawkbit.ddi.rest.api.DdiRootControllerRestApi;
|
import org.eclipse.hawkbit.ddi.rest.api.DdiRootControllerRestApi;
|
||||||
import org.eclipse.hawkbit.repository.ArtifactManagement;
|
import org.eclipse.hawkbit.repository.ArtifactManagement;
|
||||||
|
import org.eclipse.hawkbit.repository.ConfirmationManagement;
|
||||||
import org.eclipse.hawkbit.repository.ControllerManagement;
|
import org.eclipse.hawkbit.repository.ControllerManagement;
|
||||||
import org.eclipse.hawkbit.repository.EntityFactory;
|
import org.eclipse.hawkbit.repository.EntityFactory;
|
||||||
import org.eclipse.hawkbit.repository.RepositoryConstants;
|
import org.eclipse.hawkbit.repository.RepositoryConstants;
|
||||||
@@ -47,6 +54,7 @@ import org.eclipse.hawkbit.repository.event.remote.DownloadProgressEvent;
|
|||||||
import org.eclipse.hawkbit.repository.exception.ArtifactBinaryNotFoundException;
|
import org.eclipse.hawkbit.repository.exception.ArtifactBinaryNotFoundException;
|
||||||
import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException;
|
import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException;
|
||||||
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
||||||
|
import org.eclipse.hawkbit.repository.exception.InvalidConfirmationFeedbackException;
|
||||||
import org.eclipse.hawkbit.repository.exception.SoftwareModuleNotAssignedToTargetException;
|
import org.eclipse.hawkbit.repository.exception.SoftwareModuleNotAssignedToTargetException;
|
||||||
import org.eclipse.hawkbit.repository.model.Action;
|
import org.eclipse.hawkbit.repository.model.Action;
|
||||||
import org.eclipse.hawkbit.repository.model.Action.Status;
|
import org.eclipse.hawkbit.repository.model.Action.Status;
|
||||||
@@ -91,6 +99,10 @@ public class DdiRootController implements DdiRootControllerRestApi {
|
|||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(DdiRootController.class);
|
private static final Logger LOG = LoggerFactory.getLogger(DdiRootController.class);
|
||||||
private static final String GIVEN_ACTION_IS_NOT_ASSIGNED_TO_GIVEN_TARGET = "given action ({}) is not assigned to given target ({}).";
|
private static final String GIVEN_ACTION_IS_NOT_ASSIGNED_TO_GIVEN_TARGET = "given action ({}) is not assigned to given target ({}).";
|
||||||
|
private static final String FALLBACK_REMARK = "Initiated using the Device Direct Integration API without providing a remark.";
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ConfirmationManagement confirmationManagement;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ApplicationEventPublisher eventPublisher;
|
private ApplicationEventPublisher eventPublisher;
|
||||||
@@ -155,12 +167,11 @@ public class DdiRootController implements DdiRootControllerRestApi {
|
|||||||
|
|
||||||
checkAndCancelExpiredAction(activeAction);
|
checkAndCancelExpiredAction(activeAction);
|
||||||
|
|
||||||
return new ResponseEntity<>(
|
// activeAction
|
||||||
DataConversionHelper.fromTarget(target, installedAction, activeAction,
|
return new ResponseEntity<>(DataConversionHelper.fromTarget(target, installedAction, activeAction,
|
||||||
activeAction == null ? controllerManagement.getPollingTime()
|
activeAction == null ? controllerManagement.getPollingTime()
|
||||||
: controllerManagement.getPollingTimeForAction(activeAction.getId()),
|
: controllerManagement.getPollingTimeForAction(activeAction.getId()),
|
||||||
tenantAware),
|
tenantAware), HttpStatus.OK);
|
||||||
HttpStatus.OK);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -276,7 +287,7 @@ public class DdiRootController implements DdiRootControllerRestApi {
|
|||||||
|
|
||||||
checkAndCancelExpiredAction(action);
|
checkAndCancelExpiredAction(action);
|
||||||
|
|
||||||
if (!action.isCancelingOrCanceled()) {
|
if (!action.isCancelingOrCanceled() && !action.isWaitingConfirmation()) {
|
||||||
|
|
||||||
final DdiDeploymentBase base = generateDdiDeploymentBase(target, action, actionHistoryMessageCount);
|
final DdiDeploymentBase base = generateDdiDeploymentBase(target, action, actionHistoryMessageCount);
|
||||||
|
|
||||||
@@ -324,6 +335,10 @@ public class DdiRootController implements DdiRootControllerRestApi {
|
|||||||
final Target target = findTarget(controllerId);
|
final Target target = findTarget(controllerId);
|
||||||
final Action action = findActionForTarget(actionId, target);
|
final Action action = findActionForTarget(actionId, target);
|
||||||
|
|
||||||
|
if (action.isWaitingConfirmation()) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
if (!action.isActive()) {
|
if (!action.isActive()) {
|
||||||
LOG.warn("Updating action {} with feedback {} not possible since action not active anymore.",
|
LOG.warn("Updating action {} with feedback {} not possible since action not active anymore.",
|
||||||
action.getId(), feedback.getStatus());
|
action.getId(), feedback.getStatus());
|
||||||
@@ -488,28 +503,6 @@ public class DdiRootController implements DdiRootControllerRestApi {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private DdiDeploymentBase generateDdiDeploymentBase(final Target target, final Action action,
|
|
||||||
final Integer actionHistoryMessageCount) {
|
|
||||||
final List<DdiChunk> chunks = DataConversionHelper.createChunks(target, action, artifactUrlHandler,
|
|
||||||
systemManagement, new ServletServerHttpRequest(requestResponseContextHolder.getHttpServletRequest()),
|
|
||||||
controllerManagement);
|
|
||||||
|
|
||||||
final List<String> actionHistoryMsgs = controllerManagement.getActionHistoryMessages(action.getId(),
|
|
||||||
actionHistoryMessageCount == null ? Integer.parseInt(DdiRestConstants.NO_ACTION_HISTORY)
|
|
||||||
: actionHistoryMessageCount);
|
|
||||||
|
|
||||||
final DdiActionHistory actionHistory = actionHistoryMsgs.isEmpty() ? null
|
|
||||||
: new DdiActionHistory(action.getStatus().name(), actionHistoryMsgs);
|
|
||||||
|
|
||||||
final HandlingType downloadType = calculateDownloadType(action);
|
|
||||||
final HandlingType updateType = calculateUpdateType(action, downloadType);
|
|
||||||
|
|
||||||
final DdiMaintenanceWindowStatus maintenanceWindow = calculateMaintenanceWindow(action);
|
|
||||||
|
|
||||||
return new DdiDeploymentBase(Long.toString(action.getId()),
|
|
||||||
new DdiDeployment(downloadType, updateType, chunks, maintenanceWindow), actionHistory);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ActionStatusCreate generateActionCancelStatus(final DdiActionFeedback feedback, final Target target,
|
private static ActionStatusCreate generateActionCancelStatus(final DdiActionFeedback feedback, final Target target,
|
||||||
final Long actionId, final EntityFactory entityFactory) {
|
final Long actionId, final EntityFactory entityFactory) {
|
||||||
|
|
||||||
@@ -621,4 +614,150 @@ public class DdiRootController implements DdiRootControllerRestApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<DdiConfirmationBaseAction> getConfirmationBaseAction(
|
||||||
|
@PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId,
|
||||||
|
@PathVariable("actionId") final Long actionId,
|
||||||
|
@RequestParam(value = "c", required = false, defaultValue = "-1") final int resource,
|
||||||
|
@RequestParam(value = "actionHistory", defaultValue = DdiRestConstants.NO_ACTION_HISTORY) final Integer actionHistoryMessageCount) {
|
||||||
|
LOG.debug("getConfirmationBaseAction({},{})", controllerId, resource);
|
||||||
|
|
||||||
|
final Target target = findTarget(controllerId);
|
||||||
|
final Action action = findActionForTarget(actionId, target);
|
||||||
|
|
||||||
|
checkAndCancelExpiredAction(action);
|
||||||
|
|
||||||
|
if (!action.isCancelingOrCanceled() && action.isWaitingConfirmation()) {
|
||||||
|
|
||||||
|
final DdiConfirmationBaseAction base = generateDdiConfirmationBase(target, action,
|
||||||
|
actionHistoryMessageCount);
|
||||||
|
|
||||||
|
LOG.debug("Found an active UpdateAction for target {}. Returning confirmation: {}", controllerId, base);
|
||||||
|
|
||||||
|
return new ResponseEntity<>(base, HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private DdiDeploymentBase generateDdiDeploymentBase(final Target target, final Action action,
|
||||||
|
final Integer actionHistoryMessageCount) {
|
||||||
|
final DdiActionHistory actionHistory = generateDdiActionHistory(action, actionHistoryMessageCount).orElse(null);
|
||||||
|
final DdiDeployment ddiDeployment = generateDdiDeployment(target, action);
|
||||||
|
return new DdiDeploymentBase(Long.toString(action.getId()), ddiDeployment, actionHistory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DdiConfirmationBaseAction generateDdiConfirmationBase(final Target target, final Action action,
|
||||||
|
final Integer actionHistoryMessageCount) {
|
||||||
|
final DdiActionHistory actionHistory = generateDdiActionHistory(action, actionHistoryMessageCount).orElse(null);
|
||||||
|
final DdiDeployment ddiDeployment = generateDdiDeployment(target, action);
|
||||||
|
return new DdiConfirmationBaseAction(Long.toString(action.getId()), ddiDeployment, actionHistory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DdiDeployment generateDdiDeployment(final Target target, final Action action) {
|
||||||
|
final List<DdiChunk> chunks = DataConversionHelper.createChunks(target, action, artifactUrlHandler,
|
||||||
|
systemManagement, new ServletServerHttpRequest(requestResponseContextHolder.getHttpServletRequest()),
|
||||||
|
controllerManagement);
|
||||||
|
final HandlingType downloadType = calculateDownloadType(action);
|
||||||
|
final HandlingType updateType = calculateUpdateType(action, downloadType);
|
||||||
|
final DdiMaintenanceWindowStatus maintenanceWindow = calculateMaintenanceWindow(action);
|
||||||
|
return new DdiDeployment(downloadType, updateType, chunks, maintenanceWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<DdiActionHistory> generateDdiActionHistory(final Action action,
|
||||||
|
final Integer actionHistoryMessageCount) {
|
||||||
|
final List<String> actionHistoryMsgs = controllerManagement.getActionHistoryMessages(action.getId(),
|
||||||
|
actionHistoryMessageCount == null ? Integer.parseInt(DdiRestConstants.NO_ACTION_HISTORY)
|
||||||
|
: actionHistoryMessageCount);
|
||||||
|
return actionHistoryMsgs.isEmpty() ? Optional.empty()
|
||||||
|
: Optional.of(new DdiActionHistory(action.getStatus().name(), actionHistoryMsgs));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<Void> postConfirmationActionFeedback(
|
||||||
|
@Valid @RequestBody final DdiConfirmationFeedback feedback, @PathVariable("tenant") final String tenant,
|
||||||
|
@PathVariable("controllerId") final String controllerId,
|
||||||
|
@PathVariable("actionId") @NotEmpty final Long actionId) {
|
||||||
|
LOG.debug("provideConfirmationActionFeedback with feedback [controllerId={}, actionId={}]: {}", controllerId,
|
||||||
|
actionId, feedback);
|
||||||
|
|
||||||
|
final Target target = findTarget(controllerId);
|
||||||
|
final Action action = findActionForTarget(actionId, target);
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
switch (feedback.getConfirmation()) {
|
||||||
|
case CONFIRMED:
|
||||||
|
LOG.info("Controller confirmed the action (actionId: {}, controllerId: {}) as we got {} report.",
|
||||||
|
actionId, controllerId, feedback.getConfirmation());
|
||||||
|
confirmationManagement.confirmAction(actionId, feedback.getCode(), feedback.getDetails());
|
||||||
|
break;
|
||||||
|
case DENIED:
|
||||||
|
default:
|
||||||
|
LOG.debug("Controller denied the action (actionId: {}, controllerId: {}) as we got {} report.",
|
||||||
|
actionId, controllerId, feedback.getConfirmation());
|
||||||
|
confirmationManagement.denyAction(actionId, feedback.getCode(), feedback.getDetails());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (final InvalidConfirmationFeedbackException e) {
|
||||||
|
if (e.getReason() == InvalidConfirmationFeedbackException.Reason.ACTION_CLOSED) {
|
||||||
|
LOG.warn("Updating action {} with confirmation {} not possible since action not active anymore.",
|
||||||
|
action.getId(), feedback.getConfirmation());
|
||||||
|
return new ResponseEntity<>(HttpStatus.GONE);
|
||||||
|
} else if (e.getReason() == InvalidConfirmationFeedbackException.Reason.NOT_AWAITING_CONFIRMATION) {
|
||||||
|
LOG.debug("Action is not waiting for confirmation, deny request.");
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<DdiConfirmationBase> getConfirmationBase(final String tenant, final String controllerId) {
|
||||||
|
LOG.debug("getConfirmationBase is called [controllerId={}].", controllerId);
|
||||||
|
final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist(controllerId, IpUtil
|
||||||
|
.getClientIpFromRequest(requestResponseContextHolder.getHttpServletRequest(), securityProperties));
|
||||||
|
final Action activeAction = controllerManagement.findActiveActionWithHighestWeight(controllerId).orElse(null);
|
||||||
|
|
||||||
|
final DdiAutoConfirmationState autoConfirmationState = getAutoConfirmationState(controllerId);
|
||||||
|
|
||||||
|
final DdiConfirmationBase confirmationBase = DataConversionHelper.createConfirmationBase(target, activeAction,
|
||||||
|
autoConfirmationState, tenantAware);
|
||||||
|
return new ResponseEntity<>(confirmationBase, HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DdiAutoConfirmationState getAutoConfirmationState(final String controllerId) {
|
||||||
|
return confirmationManagement.getStatus(controllerId).map(status -> {
|
||||||
|
final DdiAutoConfirmationState state = DdiAutoConfirmationState.active(status.getActivatedAt());
|
||||||
|
state.setInitiator(status.getInitiator());
|
||||||
|
state.setRemark(status.getRemark());
|
||||||
|
LOG.trace("Returning state auto-conf state active [initiator='{}' | activatedAt={}] for device {}",
|
||||||
|
controllerId, status.getInitiator(), status.getActivatedAt());
|
||||||
|
return state;
|
||||||
|
}).orElseGet(() -> {
|
||||||
|
LOG.trace("Returning state auto-conf state disabled for device {}", controllerId);
|
||||||
|
return DdiAutoConfirmationState.disabled();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<Void> activateAutoConfirmation(final String tenant, final String controllerId,
|
||||||
|
final DdiActivateAutoConfirmation body) {
|
||||||
|
final String initiator = body == null ? null : body.getInitiator();
|
||||||
|
final String remark = body == null ? FALLBACK_REMARK : body.getRemark();
|
||||||
|
LOG.debug("Activate auto-confirmation request for device '{}' with payload: [initiator='{}' | remark='{}'",
|
||||||
|
controllerId, initiator, remark);
|
||||||
|
confirmationManagement.activateAutoConfirmation(controllerId, initiator, remark);
|
||||||
|
return new ResponseEntity<>(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<Void> deactivateAutoConfirmation(final String tenant, final String controllerId) {
|
||||||
|
LOG.debug("Deactivate auto-confirmation request for device ‘{}‘", controllerId);
|
||||||
|
confirmationManagement.deactivateAutoConfirmation(controllerId);
|
||||||
|
return new ResponseEntity<>(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import java.util.List;
|
|||||||
|
|
||||||
import org.apache.commons.lang3.RandomStringUtils;
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiActionFeedback;
|
import org.eclipse.hawkbit.ddi.json.model.DdiActionFeedback;
|
||||||
|
import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationFeedback;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiProgress;
|
import org.eclipse.hawkbit.ddi.json.model.DdiProgress;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiResult;
|
import org.eclipse.hawkbit.ddi.json.model.DdiResult;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiStatus;
|
import org.eclipse.hawkbit.ddi.json.model.DdiStatus;
|
||||||
@@ -68,6 +69,13 @@ public abstract class AbstractDDiApiIntegrationTest extends AbstractRestIntegrat
|
|||||||
protected static final String DEPLOYMENT_FEEDBACK = DEPLOYMENT_BASE + "/feedback";
|
protected static final String DEPLOYMENT_FEEDBACK = DEPLOYMENT_BASE + "/feedback";
|
||||||
protected static final String CANCEL_FEEDBACK = CANCEL_ACTION + "/feedback";
|
protected static final String CANCEL_FEEDBACK = CANCEL_ACTION + "/feedback";
|
||||||
|
|
||||||
|
protected static final String CONFIRMATION_BASE = CONTROLLER_BASE + "/confirmationBase";
|
||||||
|
protected static final String ACTIVATE_AUTO_CONFIRM = CONFIRMATION_BASE + "/activateAutoConfirm";
|
||||||
|
protected static final String DEACTIVATE_AUTO_CONFIRM = CONFIRMATION_BASE + "/deactivateAutoConfirm";
|
||||||
|
protected static final String CONFIRMATION_BASE_ACTION = CONTROLLER_BASE + "/confirmationBase/{actionId}";
|
||||||
|
|
||||||
|
protected static final String CONFIRMATION_FEEDBACK = CONFIRMATION_BASE_ACTION + "/feedback";
|
||||||
|
|
||||||
protected static final int ARTIFACT_SIZE = 5 * 1024;
|
protected static final int ARTIFACT_SIZE = 5 * 1024;
|
||||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
@@ -155,7 +163,7 @@ public abstract class AbstractDDiApiIntegrationTest extends AbstractRestIntegrat
|
|||||||
final Long osModuleId, final String downloadType, final String updateType) throws Exception {
|
final Long osModuleId, final String downloadType, final String updateType) throws Exception {
|
||||||
final ResultActions resultActions = performGet(DEPLOYMENT_BASE, mediaType, status().isOk(),
|
final ResultActions resultActions = performGet(DEPLOYMENT_BASE, mediaType, status().isOk(),
|
||||||
tenantAware.getCurrentTenant(), controllerId, actionId.toString());
|
tenantAware.getCurrentTenant(), controllerId, actionId.toString());
|
||||||
return verifyBasePayload(resultActions, controllerId, ds, artifact, artifactSignature, actionId, osModuleId,
|
return verifyBasePayload("$.deployment", resultActions, controllerId, ds, artifact, artifactSignature, actionId, osModuleId,
|
||||||
downloadType, updateType);
|
downloadType, updateType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,60 +179,60 @@ public abstract class AbstractDDiApiIntegrationTest extends AbstractRestIntegrat
|
|||||||
final Long osModuleId, final Action.ActionType actionType) throws Exception {
|
final Long osModuleId, final Action.ActionType actionType) throws Exception {
|
||||||
final ResultActions resultActions = performGet(INSTALLED_BASE, mediaType, status().isOk(),
|
final ResultActions resultActions = performGet(INSTALLED_BASE, mediaType, status().isOk(),
|
||||||
tenantAware.getCurrentTenant(), controllerId, actionId.toString());
|
tenantAware.getCurrentTenant(), controllerId, actionId.toString());
|
||||||
return verifyBasePayload(resultActions, controllerId, ds, artifact, artifactSignature, actionId, osModuleId,
|
return verifyBasePayload("$.deployment", resultActions, controllerId, ds, artifact, artifactSignature, actionId, osModuleId,
|
||||||
getDownloadAndUploadType(actionType), getDownloadAndUploadType(actionType));
|
getDownloadAndUploadType(actionType), getDownloadAndUploadType(actionType));
|
||||||
}
|
}
|
||||||
|
|
||||||
private ResultActions verifyBasePayload(final ResultActions resultActions, final String controllerId,
|
private ResultActions verifyBasePayload(final String prefix, final ResultActions resultActions, final String controllerId,
|
||||||
final DistributionSet ds, final Artifact artifact, final Artifact artifactSignature, final Long actionId,
|
final DistributionSet ds, final Artifact artifact, final Artifact artifactSignature, final Long actionId,
|
||||||
final Long osModuleId, final String downloadType, final String updateType) throws Exception {
|
final Long osModuleId, final String downloadType, final String updateType) throws Exception {
|
||||||
return resultActions.andExpect(jsonPath("$.id", equalTo(String.valueOf(actionId))))
|
return resultActions.andExpect(jsonPath("$.id", equalTo(String.valueOf(actionId))))
|
||||||
.andExpect(jsonPath("$.deployment.download", equalTo(downloadType)))
|
.andExpect(jsonPath(prefix + ".download", equalTo(downloadType)))
|
||||||
.andExpect(jsonPath("$.deployment.update", equalTo(updateType)))
|
.andExpect(jsonPath(prefix + ".update", equalTo(updateType)))
|
||||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='jvm')].name",
|
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='jvm')].name",
|
||||||
contains(ds.findFirstModuleByType(runtimeType).get().getName())))
|
contains(ds.findFirstModuleByType(runtimeType).get().getName())))
|
||||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='jvm')].version",
|
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='jvm')].version",
|
||||||
contains(ds.findFirstModuleByType(runtimeType).get().getVersion())))
|
contains(ds.findFirstModuleByType(runtimeType).get().getVersion())))
|
||||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].name",
|
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].name",
|
||||||
contains(ds.findFirstModuleByType(osType).get().getName())))
|
contains(ds.findFirstModuleByType(osType).get().getName())))
|
||||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].version",
|
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].version",
|
||||||
contains(ds.findFirstModuleByType(osType).get().getVersion())))
|
contains(ds.findFirstModuleByType(osType).get().getVersion())))
|
||||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[0].size", contains(ARTIFACT_SIZE)))
|
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0].size", contains(ARTIFACT_SIZE)))
|
||||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[0].filename",
|
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0].filename",
|
||||||
contains(artifact.getFilename())))
|
contains(artifact.getFilename())))
|
||||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[0].hashes.md5",
|
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0].hashes.md5",
|
||||||
contains(artifact.getMd5Hash())))
|
contains(artifact.getMd5Hash())))
|
||||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[0].hashes.sha1",
|
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0].hashes.sha1",
|
||||||
contains(artifact.getSha1Hash())))
|
contains(artifact.getSha1Hash())))
|
||||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[0].hashes.sha256",
|
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0].hashes.sha256",
|
||||||
contains(artifact.getSha256Hash())))
|
contains(artifact.getSha256Hash())))
|
||||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[0]._links.download-http.href",
|
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0]._links.download-http.href",
|
||||||
contains(HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId
|
contains(HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId
|
||||||
+ "/softwaremodules/" + osModuleId + "/artifacts/" + artifact.getFilename())))
|
+ "/softwaremodules/" + osModuleId + "/artifacts/" + artifact.getFilename())))
|
||||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[0]._links.md5sum-http.href",
|
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0]._links.md5sum-http.href",
|
||||||
contains(HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId
|
contains(HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId
|
||||||
+ "/softwaremodules/" + osModuleId + "/artifacts/" + artifact.getFilename()
|
+ "/softwaremodules/" + osModuleId + "/artifacts/" + artifact.getFilename()
|
||||||
+ ".MD5SUM")))
|
+ ".MD5SUM")))
|
||||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[1].size", contains(ARTIFACT_SIZE)))
|
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1].size", contains(ARTIFACT_SIZE)))
|
||||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[1].filename",
|
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1].filename",
|
||||||
contains(artifactSignature.getFilename())))
|
contains(artifactSignature.getFilename())))
|
||||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[1].hashes.md5",
|
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1].hashes.md5",
|
||||||
contains(artifactSignature.getMd5Hash())))
|
contains(artifactSignature.getMd5Hash())))
|
||||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[1].hashes.sha1",
|
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1].hashes.sha1",
|
||||||
contains(artifactSignature.getSha1Hash())))
|
contains(artifactSignature.getSha1Hash())))
|
||||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[1].hashes.sha256",
|
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1].hashes.sha256",
|
||||||
contains(artifactSignature.getSha256Hash())))
|
contains(artifactSignature.getSha256Hash())))
|
||||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[1]._links.download-http.href",
|
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1]._links.download-http.href",
|
||||||
contains(HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId
|
contains(HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId
|
||||||
+ "/softwaremodules/" + osModuleId + "/artifacts/" + artifactSignature.getFilename())))
|
+ "/softwaremodules/" + osModuleId + "/artifacts/" + artifactSignature.getFilename())))
|
||||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[1]._links.md5sum-http.href",
|
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1]._links.md5sum-http.href",
|
||||||
contains(HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId
|
contains(HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId
|
||||||
+ "/softwaremodules/" + osModuleId + "/artifacts/" + artifactSignature.getFilename()
|
+ "/softwaremodules/" + osModuleId + "/artifacts/" + artifactSignature.getFilename()
|
||||||
+ ".MD5SUM")))
|
+ ".MD5SUM")))
|
||||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='bApp')].version",
|
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='bApp')].version",
|
||||||
contains(ds.findFirstModuleByType(appType).get().getVersion())))
|
contains(ds.findFirstModuleByType(appType).get().getVersion())))
|
||||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='bApp')].metadata").doesNotExist())
|
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='bApp')].metadata").doesNotExist())
|
||||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='bApp')].name")
|
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='bApp')].name")
|
||||||
.value(ds.findFirstModuleByType(appType).get().getName()));
|
.value(ds.findFirstModuleByType(appType).get().getName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,4 +346,24 @@ public abstract class AbstractDDiApiIntegrationTest extends AbstractRestIntegrat
|
|||||||
messages);
|
messages);
|
||||||
return objectMapper.writeValueAsString(new DdiActionFeedback(Instant.now().toString(), ddiStatus));
|
return objectMapper.writeValueAsString(new DdiActionFeedback(Instant.now().toString(), ddiStatus));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected String getJsonConfirmationFeedback(final DdiConfirmationFeedback.Confirmation confirmation,
|
||||||
|
final Integer code, final List<String> messages) throws JsonProcessingException {
|
||||||
|
return objectMapper.writeValueAsString(new DdiConfirmationFeedback(confirmation, code, messages));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static ObjectMapper getMapper(){
|
||||||
|
return objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ResultActions getAndVerifyConfirmationBasePayload(final String controllerId, final MediaType mediaType,
|
||||||
|
final DistributionSet ds, final Artifact artifact, final Artifact artifactSignature, final Long actionId,
|
||||||
|
final Long osModuleId, final String downloadType, final String updateType) throws Exception {
|
||||||
|
final ResultActions resultActions = performGet(CONFIRMATION_BASE_ACTION, mediaType, status().isOk(),
|
||||||
|
tenantAware.getCurrentTenant(), controllerId, actionId.toString());
|
||||||
|
return verifyBasePayload("$.confirmation", resultActions, controllerId, ds, artifact, artifactSignature, actionId, osModuleId,
|
||||||
|
downloadType, updateType);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,488 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2022 Bosch.IO 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.ddi.rest.resource;
|
||||||
|
|
||||||
|
import io.qameta.allure.Description;
|
||||||
|
import io.qameta.allure.Feature;
|
||||||
|
import io.qameta.allure.Step;
|
||||||
|
import io.qameta.allure.Story;
|
||||||
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
|
import org.apache.commons.lang3.RandomUtils;
|
||||||
|
import org.eclipse.hawkbit.ddi.json.model.DdiActivateAutoConfirmation;
|
||||||
|
import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationFeedback;
|
||||||
|
import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
|
||||||
|
import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent;
|
||||||
|
import org.eclipse.hawkbit.repository.event.remote.TargetPollEvent;
|
||||||
|
import org.eclipse.hawkbit.repository.event.remote.entity.ActionCreatedEvent;
|
||||||
|
import org.eclipse.hawkbit.repository.event.remote.entity.ActionUpdatedEvent;
|
||||||
|
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetCreatedEvent;
|
||||||
|
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent;
|
||||||
|
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
|
||||||
|
import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
|
||||||
|
import org.eclipse.hawkbit.repository.event.remote.entity.TenantConfigurationCreatedEvent;
|
||||||
|
import org.eclipse.hawkbit.repository.model.Action;
|
||||||
|
import org.eclipse.hawkbit.repository.model.ActionStatus;
|
||||||
|
import org.eclipse.hawkbit.repository.model.Artifact;
|
||||||
|
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||||
|
import org.eclipse.hawkbit.repository.model.Target;
|
||||||
|
import org.eclipse.hawkbit.repository.test.matcher.Expect;
|
||||||
|
import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents;
|
||||||
|
import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.hateoas.MediaTypes;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.ResultActions;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.eclipse.hawkbit.repository.jpa.JpaConfirmationManagement.CONFIRMATION_CODE_MSG_PREFIX;
|
||||||
|
import static org.hamcrest.CoreMatchers.equalTo;
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.hamcrest.Matchers.hasItem;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test confirmation base from the controller.
|
||||||
|
*/
|
||||||
|
@Feature("Component Tests - Direct Device Integration API")
|
||||||
|
@Story("Confirmation Action Resource")
|
||||||
|
public class DdiConfirmationBaseTest extends AbstractDDiApiIntegrationTest {
|
||||||
|
|
||||||
|
private static final String DEFAULT_CONTROLLER_ID = "4747";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Forced deployment to a controller. Checks if the confirmation resource response payload for a given"
|
||||||
|
+ " deployment is as expected.")
|
||||||
|
public void verifyConfirmationReferencesInControllerBase() throws Exception {
|
||||||
|
enableConfirmationFlow();
|
||||||
|
// Prepare test data
|
||||||
|
final DistributionSet ds = testdataFactory.createDistributionSet("", true);
|
||||||
|
final DistributionSet ds2 = testdataFactory.createDistributionSet("2", true);
|
||||||
|
final Artifact artifact = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE), getOsModule(ds),
|
||||||
|
"test1", ARTIFACT_SIZE);
|
||||||
|
final Artifact artifactSignature = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||||
|
getOsModule(ds), "test1.signature", ARTIFACT_SIZE);
|
||||||
|
|
||||||
|
final Target savedTarget = testdataFactory.createTarget(DdiConfirmationBaseTest.DEFAULT_CONTROLLER_ID);
|
||||||
|
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).isEmpty();
|
||||||
|
|
||||||
|
final List<Target> targetsAssignedToDs = assignDistributionSet(ds.getId(), savedTarget.getControllerId(),
|
||||||
|
Action.ActionType.FORCED).getAssignedEntity().stream().map(Action::getTarget)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||||
|
|
||||||
|
final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||||
|
.getContent().get(0);
|
||||||
|
assertThat(deploymentManagement.countActionsAll()).isEqualTo(1);
|
||||||
|
|
||||||
|
assignDistributionSet(ds2, targetsAssignedToDs).getAssignedEntity();
|
||||||
|
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(2);
|
||||||
|
assertThat(deploymentManagement.countActionsAll()).isEqualTo(2);
|
||||||
|
|
||||||
|
final Action uaction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||||
|
.getContent().get(0);
|
||||||
|
assertThat(uaction.getDistributionSet()).isEqualTo(ds);
|
||||||
|
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(2);
|
||||||
|
|
||||||
|
// Run test
|
||||||
|
final long current = System.currentTimeMillis();
|
||||||
|
final String expectedConfirmationBaseLink = String.format("/%s/controller/v1/%s/confirmationBase/%d",
|
||||||
|
tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID, uaction.getId());
|
||||||
|
|
||||||
|
performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(),
|
||||||
|
DEFAULT_CONTROLLER_ID)
|
||||||
|
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||||
|
.andExpect(jsonPath("$._links.confirmationBase.href",
|
||||||
|
containsString(expectedConfirmationBaseLink)))
|
||||||
|
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist());
|
||||||
|
|
||||||
|
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||||
|
.isGreaterThanOrEqualTo(current);
|
||||||
|
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||||
|
.isLessThanOrEqualTo(System.currentTimeMillis());
|
||||||
|
assertThat(deploymentManagement.countActionStatusAll()).isEqualTo(2);
|
||||||
|
|
||||||
|
final DistributionSet findDistributionSetByAction = distributionSetManagement.getByAction(action.getId()).get();
|
||||||
|
|
||||||
|
getAndVerifyConfirmationBasePayload(DEFAULT_CONTROLLER_ID, MediaType.APPLICATION_JSON, ds, artifact,
|
||||||
|
artifactSignature, action.getId(),
|
||||||
|
findDistributionSetByAction.findFirstModuleByType(osType).get().getId(), "forced", "forced");
|
||||||
|
|
||||||
|
// Retrieved is reported
|
||||||
|
final Iterable<ActionStatus> actionStatus = deploymentManagement
|
||||||
|
.findActionStatusByAction(PageRequest.of(0, 100, Sort.Direction.DESC, "id"), uaction.getId());
|
||||||
|
assertThat(actionStatus).hasSize(1)
|
||||||
|
.allMatch(status -> status.getStatus() == Action.Status.WAIT_FOR_CONFIRMATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Ensure that the deployment resource is available as CBOR")
|
||||||
|
public void confirmationResourceCbor() throws Exception {
|
||||||
|
enableConfirmationFlow();
|
||||||
|
final Target target = testdataFactory.createTarget();
|
||||||
|
final DistributionSet distributionSet = testdataFactory.createDistributionSet("");
|
||||||
|
|
||||||
|
assignDistributionSet(distributionSet.getId(), target.getName());
|
||||||
|
final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId())
|
||||||
|
.getContent().get(0);
|
||||||
|
|
||||||
|
// get confirmation base
|
||||||
|
performGet(CONFIRMATION_BASE_ACTION, MediaType.parseMediaType(DdiRestConstants.MEDIA_TYPE_CBOR),
|
||||||
|
status().isOk(), tenantAware.getCurrentTenant(), target.getControllerId(), action.getId().toString());
|
||||||
|
|
||||||
|
final Long softwareModuleId = distributionSet.getModules().stream().findAny().get().getId();
|
||||||
|
testdataFactory.createArtifacts(softwareModuleId);
|
||||||
|
// get artifacts
|
||||||
|
performGet(SOFTWARE_MODULE_ARTIFACTS, MediaType.parseMediaType(DdiRestConstants.MEDIA_TYPE_CBOR),
|
||||||
|
status().isOk(), tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||||
|
String.valueOf(softwareModuleId));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Ensure that the confirmation endpoint is not available.")
|
||||||
|
public void confirmationEndpointNotExposed() throws Exception {
|
||||||
|
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||||
|
Target savedTarget = testdataFactory.createTarget("988");
|
||||||
|
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||||
|
|
||||||
|
final String controllerId = savedTarget.getControllerId();
|
||||||
|
|
||||||
|
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, controllerId).getContent()
|
||||||
|
.get(0);
|
||||||
|
|
||||||
|
mvc.perform(
|
||||||
|
get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId).accept(MediaType.APPLICATION_JSON))
|
||||||
|
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$._links.confirmationBase.href").doesNotExist());
|
||||||
|
|
||||||
|
mvc.perform(get(CONFIRMATION_BASE_ACTION, tenantAware.getCurrentTenant(), controllerId, savedAction.getId())
|
||||||
|
.accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
|
||||||
|
.andExpect(status().isNotFound());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Ensure that the deploymentBase endpoint is not available for action ins WFC state.")
|
||||||
|
public void deploymentEndpointNotAccessibleForActionsWFC() throws Exception {
|
||||||
|
enableConfirmationFlow();
|
||||||
|
|
||||||
|
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||||
|
Target savedTarget = testdataFactory.createTarget("988");
|
||||||
|
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||||
|
|
||||||
|
final String controllerId = savedTarget.getControllerId();
|
||||||
|
|
||||||
|
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, controllerId).getContent()
|
||||||
|
.get(0);
|
||||||
|
|
||||||
|
mvc.perform(
|
||||||
|
get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId).accept(MediaType.APPLICATION_JSON))
|
||||||
|
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$._links.confirmationBase.href").exists())
|
||||||
|
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist());
|
||||||
|
|
||||||
|
mvc.perform(get(CONFIRMATION_BASE_ACTION, tenantAware.getCurrentTenant(), controllerId, savedAction.getId())
|
||||||
|
.accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||||
|
|
||||||
|
mvc.perform(get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), controllerId, savedAction.getId())
|
||||||
|
.accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
|
||||||
|
.andExpect(status().isNotFound());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Ensure that the confirmation endpoints are still available after deactivating the confirmation flow.")
|
||||||
|
public void verifyConfirmationBaseEndpointsArePresentAfterDisablingConfirmationFlow() throws Exception {
|
||||||
|
enableConfirmationFlow();
|
||||||
|
|
||||||
|
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||||
|
Target savedTarget = testdataFactory.createTarget("988");
|
||||||
|
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||||
|
|
||||||
|
final String controllerId = savedTarget.getControllerId();
|
||||||
|
|
||||||
|
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, controllerId).getContent()
|
||||||
|
.get(0);
|
||||||
|
|
||||||
|
// disable confirmation flow
|
||||||
|
disableConfirmationFlow();
|
||||||
|
|
||||||
|
// confirmation base should still be exposed
|
||||||
|
verifyActionInConfirmationBaseState(savedTarget.getControllerId(), savedAction.getId());
|
||||||
|
|
||||||
|
// verify confirmation endpoint is still accessible
|
||||||
|
sendConfirmationFeedback(savedTarget, savedAction, DdiConfirmationFeedback.Confirmation.DENIED, 20,
|
||||||
|
"Action denied message.").andExpect(status().isOk());
|
||||||
|
|
||||||
|
// confirmation base should still be exposed
|
||||||
|
verifyActionInConfirmationBaseState(savedTarget.getControllerId(), savedAction.getId());
|
||||||
|
|
||||||
|
// verify confirmation endpoint is still accessible
|
||||||
|
sendConfirmationFeedback(savedTarget, savedAction, DdiConfirmationFeedback.Confirmation.CONFIRMED, 10,
|
||||||
|
"Action confirmed message.").andExpect(status().isOk());
|
||||||
|
|
||||||
|
// assert deployment link is exposed to the target
|
||||||
|
verifyActionInDeploymentBaseState(controllerId, savedAction.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Controller sends a confirmed action state.")
|
||||||
|
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
|
||||||
|
@Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 2),
|
||||||
|
@Expect(type = TargetUpdatedEvent.class, count = 1),
|
||||||
|
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||||
|
@Expect(type = TargetUpdatedEvent.class, count = 1), @Expect(type = TargetPollEvent.class, count = 1),
|
||||||
|
@Expect(type = TenantConfigurationCreatedEvent.class, count = 1) })
|
||||||
|
void sendConfirmedActionStateFeedbackTest() throws Exception {
|
||||||
|
enableConfirmationFlow();
|
||||||
|
|
||||||
|
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||||
|
Target savedTarget = testdataFactory.createTarget("988");
|
||||||
|
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||||
|
|
||||||
|
String controllerId = savedTarget.getControllerId();
|
||||||
|
|
||||||
|
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, controllerId).getContent()
|
||||||
|
.get(0);
|
||||||
|
|
||||||
|
sendConfirmationFeedback(savedTarget, savedAction, DdiConfirmationFeedback.Confirmation.CONFIRMED, 10,
|
||||||
|
"Action confirmed message.").andExpect(status().isOk());
|
||||||
|
|
||||||
|
// assert deployment link is exposed to the target
|
||||||
|
verifyActionInDeploymentBaseState(controllerId, savedAction.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Step
|
||||||
|
private void verifyActionInDeploymentBaseState(final String controllerId, final long actionId) throws Exception {
|
||||||
|
final String expectedDeploymentBaseLink = String.format("/%s/controller/v1/%s/deploymentBase/%d",
|
||||||
|
tenantAware.getCurrentTenant(), controllerId, actionId);
|
||||||
|
|
||||||
|
mvc.perform(
|
||||||
|
get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId).accept(MediaType.APPLICATION_JSON))
|
||||||
|
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$._links.deploymentBase.href", containsString(expectedDeploymentBaseLink)))
|
||||||
|
.andExpect(jsonPath("$._links.confirmationBase.href").doesNotExist());
|
||||||
|
|
||||||
|
// assert that deployment endpoint is working
|
||||||
|
mvc.perform(get(expectedDeploymentBaseLink).accept(MediaType.APPLICATION_JSON))
|
||||||
|
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Step
|
||||||
|
private void verifyActionInConfirmationBaseState(final String controllerId, final long actionId) throws Exception {
|
||||||
|
mvc.perform(
|
||||||
|
get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId).accept(MediaType.APPLICATION_JSON))
|
||||||
|
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$._links.confirmationBase.href").exists())
|
||||||
|
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist());
|
||||||
|
|
||||||
|
mvc.perform(get(CONFIRMATION_BASE_ACTION, tenantAware.getCurrentTenant(), controllerId, actionId)
|
||||||
|
.accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||||
|
|
||||||
|
mvc.perform(get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), controllerId, actionId)
|
||||||
|
.accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
|
||||||
|
.andExpect(status().isNotFound());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Confirmation base provides right values if auto-confirm not active.")
|
||||||
|
void getConfirmationBaseProvidesAutoConfirmStatusNotActive() throws Exception {
|
||||||
|
enableConfirmationFlow();
|
||||||
|
|
||||||
|
final String controllerId = testdataFactory.createTarget("989").getControllerId();
|
||||||
|
assignDistributionSet(testdataFactory.createDistributionSet("").getId(), controllerId);
|
||||||
|
final long actionId = deploymentManagement.findActiveActionsByTarget(PAGE, controllerId).getContent().get(0)
|
||||||
|
.getId();
|
||||||
|
|
||||||
|
final String confirmationBaseActionLink = String.format("/%s/controller/v1/%s/confirmationBase/%d",
|
||||||
|
tenantAware.getCurrentTenant(), controllerId, actionId);
|
||||||
|
|
||||||
|
final String activateAutoConfLink = String.format("/%s/controller/v1/%s/confirmationBase/activateAutoConfirm",
|
||||||
|
tenantAware.getCurrentTenant(), controllerId);
|
||||||
|
|
||||||
|
mvc.perform(
|
||||||
|
get(CONFIRMATION_BASE, tenantAware.getCurrentTenant(), controllerId).accept(MediaType.APPLICATION_JSON))
|
||||||
|
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("autoConfirm.active", equalTo(Boolean.FALSE)))
|
||||||
|
.andExpect(jsonPath("$._links.confirmationBase.href", containsString(confirmationBaseActionLink)))
|
||||||
|
.andExpect(jsonPath("$._links.activateAutoConfirm.href", containsString(activateAutoConfLink)))
|
||||||
|
.andExpect(jsonPath("$._links.deactivateAutoConfirm").doesNotExist());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("possibleActiveStates")
|
||||||
|
@Description("Confirmation base provides right values if auto-confirm is active.")
|
||||||
|
void getConfirmationBaseProvidesAutoConfirmStatusActive(final String initiator, final String remark)
|
||||||
|
throws Exception {
|
||||||
|
final String controllerId = testdataFactory.createTarget("988").getControllerId();
|
||||||
|
|
||||||
|
confirmationManagement.activateAutoConfirmation(controllerId, initiator, remark);
|
||||||
|
|
||||||
|
final String deactivateAutoConfLink = String.format(
|
||||||
|
"/%s/controller/v1/%s/confirmationBase/deactivateAutoConfirm", tenantAware.getCurrentTenant(),
|
||||||
|
controllerId);
|
||||||
|
|
||||||
|
mvc.perform(
|
||||||
|
get(CONFIRMATION_BASE, tenantAware.getCurrentTenant(), controllerId).accept(MediaType.APPLICATION_JSON))
|
||||||
|
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("autoConfirm.active", equalTo(Boolean.TRUE)))
|
||||||
|
.andExpect(initiator == null ? jsonPath("autoConfirm.initiator").doesNotExist()
|
||||||
|
: jsonPath("autoConfirm.initiator", equalTo(initiator)))
|
||||||
|
.andExpect(remark == null ? jsonPath("autoConfirm.remark").doesNotExist()
|
||||||
|
: jsonPath("autoConfirm.remark", equalTo(remark)))
|
||||||
|
.andExpect(jsonPath("$._links.deactivateAutoConfirm.href", containsString(deactivateAutoConfLink)))
|
||||||
|
.andExpect(jsonPath("$._links.activateAutoConfirm").doesNotExist());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> possibleActiveStates() {
|
||||||
|
return Stream.of(Arguments.of("someInitiator", "someRemark"), Arguments.of(null, "someRemark"),
|
||||||
|
Arguments.of("someInitiator", null), Arguments.of(null, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("possibleActiveStates")
|
||||||
|
@Description("Verify auto-confirm activation is handled correctly.")
|
||||||
|
void activateAutoConfirmation(final String initiator, final String remark) throws Exception {
|
||||||
|
final String controllerId = testdataFactory.createTarget("988").getControllerId();
|
||||||
|
|
||||||
|
final DdiActivateAutoConfirmation body = new DdiActivateAutoConfirmation(initiator, remark);
|
||||||
|
|
||||||
|
mvc.perform(post(ACTIVATE_AUTO_CONFIRM, tenantAware.getCurrentTenant(), controllerId)
|
||||||
|
.content(getMapper().writeValueAsString(body)).contentType(MediaType.APPLICATION_JSON_UTF8))
|
||||||
|
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||||
|
|
||||||
|
assertThat(confirmationManagement.getStatus(controllerId)).hasValueSatisfying(status -> {
|
||||||
|
assertThat(status.getInitiator()).isEqualTo(initiator);
|
||||||
|
assertThat(status.getRemark()).isEqualTo(remark);
|
||||||
|
assertThat(status.getCreatedBy()).isEqualTo("bumlux");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Verify auto-confirm deactivation is handled correctly.")
|
||||||
|
void deactivateAutoConfirmation() throws Exception {
|
||||||
|
final String controllerId = testdataFactory.createTarget("988").getControllerId();
|
||||||
|
|
||||||
|
confirmationManagement.activateAutoConfirmation(controllerId, null, null);
|
||||||
|
|
||||||
|
mvc.perform(post(DEACTIVATE_AUTO_CONFIRM, tenantAware.getCurrentTenant(), controllerId))
|
||||||
|
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||||
|
|
||||||
|
assertThat(confirmationManagement.getStatus(controllerId)).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Controller sends a denied action state.")
|
||||||
|
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
|
||||||
|
@Expect(type = ActionCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = ActionUpdatedEvent.class, count = 1),
|
||||||
|
@Expect(type = TargetUpdatedEvent.class, count = 1),
|
||||||
|
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||||
|
@Expect(type = TargetUpdatedEvent.class, count = 1),
|
||||||
|
@Expect(type = TargetPollEvent.class, count = 1),
|
||||||
|
@Expect(type = TenantConfigurationCreatedEvent.class, count = 1) })
|
||||||
|
void sendDeniedActionStateFeedbackTest() throws Exception {
|
||||||
|
enableConfirmationFlow();
|
||||||
|
|
||||||
|
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||||
|
Target savedTarget = testdataFactory.createTarget("989");
|
||||||
|
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||||
|
String controllerId = savedTarget.getControllerId();
|
||||||
|
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, controllerId).getContent()
|
||||||
|
.get(0);
|
||||||
|
|
||||||
|
sendConfirmationFeedback(savedTarget, savedAction, DdiConfirmationFeedback.Confirmation.DENIED, 10,
|
||||||
|
"Action denied message.").andExpect(status().isOk());
|
||||||
|
|
||||||
|
// asserts that deployment link is not available
|
||||||
|
final String expectedConfirmationBaseLink = String.format("/%s/controller/v1/%s/confirmationBase/%d",
|
||||||
|
tenantAware.getCurrentTenant(), controllerId, savedAction.getId());
|
||||||
|
|
||||||
|
mvc.perform(
|
||||||
|
get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId).accept(MediaType.APPLICATION_JSON))
|
||||||
|
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist())
|
||||||
|
.andExpect(jsonPath("$._links.confirmationBase.href", containsString(expectedConfirmationBaseLink)));
|
||||||
|
|
||||||
|
mvc.perform(get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), controllerId, savedAction.getId())
|
||||||
|
.accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
|
||||||
|
.andExpect(status().isNotFound());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResultActions sendConfirmationFeedback(final Target target, final Action action,
|
||||||
|
final DdiConfirmationFeedback.Confirmation confirmation, Integer code, String message) throws Exception {
|
||||||
|
|
||||||
|
if (message == null) {
|
||||||
|
message = RandomStringUtils.randomAlphanumeric(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String feedback = getJsonConfirmationFeedback(confirmation, code, Collections.singletonList(message));
|
||||||
|
return mvc.perform(
|
||||||
|
post(CONFIRMATION_FEEDBACK, tenantAware.getCurrentTenant(), target.getControllerId(), action.getId())
|
||||||
|
.content(feedback).contentType(MediaType.APPLICATION_JSON));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Description("Test to verify that only a specific count of messages are returned based on the input actionHistory for getControllerDeploymentActionFeedback endpoint.")
|
||||||
|
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||||
|
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
|
||||||
|
@Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 2),
|
||||||
|
@Expect(type = TargetUpdatedEvent.class, count = 1),
|
||||||
|
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||||
|
@Expect(type = TenantConfigurationCreatedEvent.class, count = 1) })
|
||||||
|
void testActionHistoryCount() throws Exception {
|
||||||
|
enableConfirmationFlow();
|
||||||
|
|
||||||
|
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||||
|
Target savedTarget = testdataFactory.createTarget("990");
|
||||||
|
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||||
|
|
||||||
|
String controllerId = savedTarget.getControllerId();
|
||||||
|
|
||||||
|
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, controllerId).getContent()
|
||||||
|
.get(0);
|
||||||
|
final String CONFIRMED_MESSAGE = "Action confirmed message.";
|
||||||
|
final Integer CONFIRMED_CODE = 10;
|
||||||
|
sendConfirmationFeedback(savedTarget, savedAction, DdiConfirmationFeedback.Confirmation.CONFIRMED,
|
||||||
|
CONFIRMED_CODE, CONFIRMED_MESSAGE).andExpect(status().isOk());
|
||||||
|
|
||||||
|
// confirmationBase not available in RUNNING state anymore
|
||||||
|
mvc.perform(get(CONFIRMATION_BASE_ACTION, tenantAware.getCurrentTenant(), savedTarget.getControllerId(),
|
||||||
|
savedAction.getId()).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||||
|
.andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||||
|
|
||||||
|
// assert confirmed message against deploymentBase endpoint
|
||||||
|
// this call will update the action due to retrieved action status update
|
||||||
|
mvc.perform(
|
||||||
|
get(DEPLOYMENT_BASE + "?actionHistory=2", tenantAware.getCurrentTenant(), savedTarget.getControllerId(),
|
||||||
|
savedAction.getId()).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||||
|
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.actionHistory.messages", hasItem(containsString(CONFIRMED_MESSAGE))))
|
||||||
|
.andExpect(jsonPath("$.actionHistory.messages",
|
||||||
|
hasItem(containsString(String.format(CONFIRMATION_CODE_MSG_PREFIX, CONFIRMED_CODE)))));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ public class MgmtTargetAssignmentRequestBody {
|
|||||||
private MgmtActionType type;
|
private MgmtActionType type;
|
||||||
private MgmtMaintenanceWindowRequestBody maintenanceWindow;
|
private MgmtMaintenanceWindowRequestBody maintenanceWindow;
|
||||||
private Integer weight;
|
private Integer weight;
|
||||||
|
private Boolean confirmationRequired;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JsonCreator Constructor
|
* JsonCreator Constructor
|
||||||
@@ -77,4 +78,12 @@ public class MgmtTargetAssignmentRequestBody {
|
|||||||
public void setMaintenanceWindow(final MgmtMaintenanceWindowRequestBody maintenanceWindow) {
|
public void setMaintenanceWindow(final MgmtMaintenanceWindowRequestBody maintenanceWindow) {
|
||||||
this.maintenanceWindow = maintenanceWindow;
|
this.maintenanceWindow = maintenanceWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean isConfirmationRequired() {
|
||||||
|
return confirmationRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfirmationRequired(final boolean confirmationRequired) {
|
||||||
|
this.confirmationRequired = confirmationRequired;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
package org.eclipse.hawkbit.mgmt.json.model.rollout;
|
package org.eclipse.hawkbit.mgmt.json.model.rollout;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType;
|
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType;
|
||||||
import org.eclipse.hawkbit.mgmt.json.model.rolloutgroup.MgmtRolloutGroup;
|
import org.eclipse.hawkbit.mgmt.json.model.rolloutgroup.MgmtRolloutGroup;
|
||||||
@@ -38,6 +39,9 @@ public class MgmtRolloutRestRequestBody extends AbstractMgmtRolloutConditionsEnt
|
|||||||
@JsonProperty(required = false)
|
@JsonProperty(required = false)
|
||||||
private Integer weight;
|
private Integer weight;
|
||||||
|
|
||||||
|
@JsonProperty(required = false)
|
||||||
|
private Boolean confirmationRequired;
|
||||||
|
|
||||||
private MgmtActionType type;
|
private MgmtActionType type;
|
||||||
|
|
||||||
private List<MgmtRolloutGroup> groups;
|
private List<MgmtRolloutGroup> groups;
|
||||||
@@ -161,4 +165,21 @@ public class MgmtRolloutRestRequestBody extends AbstractMgmtRolloutConditionsEnt
|
|||||||
public void setWeight(final Integer weight) {
|
public void setWeight(final Integer weight) {
|
||||||
this.weight = weight;
|
this.weight = weight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only considered if confirmation flow active
|
||||||
|
*
|
||||||
|
* @return if the confirmation is required for this rollout
|
||||||
|
*/
|
||||||
|
public Boolean isConfirmationRequired() {
|
||||||
|
return confirmationRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param confirmationRequired
|
||||||
|
* if the confirmation is required for this rollout
|
||||||
|
*/
|
||||||
|
public void setConfirmationRequired(final boolean confirmationRequired) {
|
||||||
|
this.confirmationRequired = confirmationRequired;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ public class MgmtRolloutGroup extends AbstractMgmtRolloutConditionsEntity {
|
|||||||
|
|
||||||
private String targetFilterQuery;
|
private String targetFilterQuery;
|
||||||
private Float targetPercentage;
|
private Float targetPercentage;
|
||||||
|
private Boolean confirmationRequired;
|
||||||
|
|
||||||
public String getTargetFilterQuery() {
|
public String getTargetFilterQuery() {
|
||||||
return targetFilterQuery;
|
return targetFilterQuery;
|
||||||
@@ -38,4 +39,12 @@ public class MgmtRolloutGroup extends AbstractMgmtRolloutConditionsEntity {
|
|||||||
public void setTargetPercentage(Float targetPercentage) {
|
public void setTargetPercentage(Float targetPercentage) {
|
||||||
this.targetPercentage = targetPercentage;
|
this.targetPercentage = targetPercentage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean isConfirmationRequired() {
|
||||||
|
return confirmationRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfirmationRequired(final Boolean confirmationRequired) {
|
||||||
|
this.confirmationRequired = confirmationRequired;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ public class MgmtDistributionSetAssignment extends MgmtId {
|
|||||||
private long forcetime;
|
private long forcetime;
|
||||||
@JsonProperty(required = false)
|
@JsonProperty(required = false)
|
||||||
private Integer weight;
|
private Integer weight;
|
||||||
|
@JsonProperty(required = false)
|
||||||
|
private Boolean confirmationRequired;
|
||||||
private MgmtActionType type;
|
private MgmtActionType type;
|
||||||
private MgmtMaintenanceWindowRequestBody maintenanceWindow;
|
private MgmtMaintenanceWindowRequestBody maintenanceWindow;
|
||||||
|
|
||||||
@@ -77,4 +79,12 @@ public class MgmtDistributionSetAssignment extends MgmtId {
|
|||||||
public void setMaintenanceWindow(final MgmtMaintenanceWindowRequestBody maintenanceWindow) {
|
public void setMaintenanceWindow(final MgmtMaintenanceWindowRequestBody maintenanceWindow) {
|
||||||
this.maintenanceWindow = maintenanceWindow;
|
this.maintenanceWindow = maintenanceWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean isConfirmationRequired() {
|
||||||
|
return confirmationRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfirmationRequired(final Boolean confirmationRequired) {
|
||||||
|
this.confirmationRequired = confirmationRequired;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ public class MgmtTarget extends MgmtNamedEntity {
|
|||||||
@JsonProperty
|
@JsonProperty
|
||||||
private String targetTypeName;
|
private String targetTypeName;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private Boolean autoConfirmActive;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Target type ID
|
* @return Target type ID
|
||||||
*/
|
*/
|
||||||
@@ -221,4 +224,13 @@ public class MgmtTarget extends MgmtNamedEntity {
|
|||||||
public void setRequestAttributes(final boolean requestAttributes) {
|
public void setRequestAttributes(final boolean requestAttributes) {
|
||||||
this.requestAttributes = requestAttributes;
|
this.requestAttributes = requestAttributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean getAutoConfirmActive() {
|
||||||
|
return autoConfirmActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public void setAutoConfirmActive(final boolean autoConfirmActive) {
|
||||||
|
this.autoConfirmActive = autoConfirmActive;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user