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:
Michael Herdt
2023-01-25 12:11:05 +01:00
committed by GitHub
parent b919ceda5c
commit 21f1569881
208 changed files with 7830 additions and 831 deletions

View File

@@ -159,6 +159,7 @@ public class SecurityManagedConfiguration {
static class ControllerSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
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}/installedBase/**",
DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/cancelAction/**",

View File

@@ -20,10 +20,23 @@ public enum SpServerError {
SP_REPO_GENERIC_ERROR("hawkbit.server.error.repo.genericError", "unknown error occurred"),
/**
*
*/
*
*/
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"),
/**
*

View File

@@ -14,6 +14,7 @@ import org.eclipse.hawkbit.api.HostnameResolver;
import org.eclipse.hawkbit.cache.DownloadIdCache;
import org.eclipse.hawkbit.dmf.amqp.api.AmqpSettings;
import org.eclipse.hawkbit.repository.ArtifactManagement;
import org.eclipse.hawkbit.repository.ConfirmationManagement;
import org.eclipse.hawkbit.repository.ControllerManagement;
import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.DistributionSetManagement;
@@ -275,9 +276,10 @@ public class AmqpConfiguration {
final AmqpMessageDispatcherService amqpMessageDispatcherService,
final ControllerManagement controllerManagement, final EntityFactory entityFactory,
final SystemSecurityContext systemSecurityContext,
final TenantConfigurationManagement tenantConfigurationManagement) {
final TenantConfigurationManagement tenantConfigurationManagement,
final ConfirmationManagement confirmationManagement) {
return new AmqpMessageHandlerService(rabbitTemplate, amqpMessageDispatcherService, controllerManagement,
entityFactory, systemSecurityContext, tenantConfigurationManagement);
entityFactory, systemSecurityContext, tenantConfigurationManagement, confirmationManagement);
}
/**

View File

@@ -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.DmfArtifactHash;
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.DmfMetadata;
import org.eclipse.hawkbit.dmf.json.model.DmfMultiActionRequest;
@@ -273,6 +274,8 @@ public class AmqpMessageDispatcherService extends BaseAmqpService {
final Map<SoftwareModule, List<SoftwareModuleMetadata>> softwareModules) {
if (action.isCancelingOrCanceled()) {
return createPlainActionRequest(action);
} else if (action.isWaitingConfirmation()) {
return createConfirmRequest(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.
*/
private static EventTopic getEventTypeForTarget(final ActionProperties action) {
if (action.isWaitingConfirmation()) {
return EventTopic.CONFIRM;
}
return (Action.ActionType.DOWNLOAD_ONLY == action.getActionType() || !action.isMaintenanceWindowAvailable())
? EventTopic.DOWNLOAD
: EventTopic.DOWNLOAD_AND_INSTALL;
@@ -415,9 +421,16 @@ public class AmqpMessageDispatcherService extends BaseAmqpService {
return;
}
final DmfDownloadAndUpdateRequest downloadAndUpdateRequest = createDownloadAndUpdateRequest(target,
action.getId(), modules);
final Message message = getMessageConverter().toMessage(downloadAndUpdateRequest,
DmfActionRequest request;
if (action.isWaitingConfirmation()) {
// 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)));
amqpSenderService.sendMessage(message, targetAddress);
}
@@ -624,4 +637,25 @@ public class AmqpMessageDispatcherService extends BaseAmqpService {
? EventTopic.BATCH_DOWNLOAD
: 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;
}
}

View File

@@ -24,12 +24,15 @@ import java.util.stream.Collectors;
import org.eclipse.hawkbit.dmf.amqp.api.EventTopic;
import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey;
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.DmfAttributeUpdate;
import org.eclipse.hawkbit.dmf.json.model.DmfAutoConfirmation;
import org.eclipse.hawkbit.dmf.json.model.DmfCreateThing;
import org.eclipse.hawkbit.dmf.json.model.DmfUpdateMode;
import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions;
import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails;
import org.eclipse.hawkbit.repository.ConfirmationManagement;
import org.eclipse.hawkbit.repository.ControllerManagement;
import org.eclipse.hawkbit.repository.EntityFactory;
import org.eclipse.hawkbit.repository.RepositoryConstants;
@@ -75,6 +78,7 @@ public class AmqpMessageHandlerService extends BaseAmqpService {
private final AmqpMessageDispatcherService amqpMessageDispatcherService;
private ControllerManagement controllerManagement;
private ConfirmationManagement confirmationManagement;
private final EntityFactory entityFactory;
@@ -101,18 +105,21 @@ public class AmqpMessageHandlerService extends BaseAmqpService {
* the system Security Context
* @param tenantConfigurationManagement
* the tenant configuration Management
* @param confirmationManagement
* the confirmation management
*/
public AmqpMessageHandlerService(final RabbitTemplate rabbitTemplate,
final AmqpMessageDispatcherService amqpMessageDispatcherService,
final ControllerManagement controllerManagement, final EntityFactory entityFactory,
final SystemSecurityContext systemSecurityContext,
final TenantConfigurationManagement tenantConfigurationManagement) {
final TenantConfigurationManagement tenantConfigurationManagement, final ConfirmationManagement confirmationManagement) {
super(rabbitTemplate);
this.amqpMessageDispatcherService = amqpMessageDispatcherService;
this.controllerManagement = controllerManagement;
this.entityFactory = entityFactory;
this.systemSecurityContext = systemSecurityContext;
this.tenantConfigurationManagement = tenantConfigurationManagement;
this.confirmationManagement = confirmationManagement;
}
/**
@@ -314,6 +321,9 @@ public class AmqpMessageHandlerService extends BaseAmqpService {
case UPDATE_ATTRIBUTES:
updateAttributes(message);
break;
case UPDATE_AUTO_CONFIRM:
setAutoConfirmationState(message);
break;
default:
logAndThrowMessageError(message, "Got event without appropriate topic.");
break;
@@ -331,7 +341,23 @@ public class AmqpMessageHandlerService extends BaseAmqpService {
final String thingId = getStringHeaderKey(message, MessageHeaderKey.THING_ID, THING_ID_NULL);
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 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);
});
final Action updatedAction;
final Action updatedAction = (Status.CANCELED == status)
? controllerManagement.addCancelActionStatus(actionStatus)
: controllerManagement.addUpdateActionStatus(actionStatus);
if (actionUpdateStatus.getActionStatus() == DmfActionStatus.CONFIRMED) {
updatedAction = confirmationManagement.confirmAction(action.getId(),
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());
}
}
@@ -391,6 +425,7 @@ public class AmqpMessageHandlerService extends BaseAmqpService {
status = Status.RETRIEVED;
break;
case RUNNING:
case CONFIRMED:
status = Status.RUNNING;
break;
case CANCELED:
@@ -411,6 +446,9 @@ public class AmqpMessageHandlerService extends BaseAmqpService {
case CANCEL_REJECTED:
status = handleCancelRejectedState(message, action);
break;
case DENIED:
status = Status.WAIT_FOR_CONFIRMATION;
break;
default:
logAndThrowMessageError(message, "Status for action does not exisit.");
}

View File

@@ -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.json.model.DmfDownloadResponse;
import org.eclipse.hawkbit.repository.ArtifactManagement;
import org.eclipse.hawkbit.repository.ConfirmationManagement;
import org.eclipse.hawkbit.repository.ControllerManagement;
import org.eclipse.hawkbit.repository.SystemManagement;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
@@ -117,6 +118,9 @@ public class AmqpControllerAuthenticationTest {
@Mock
private ControllerManagement controllerManagement;
@Mock
private ConfirmationManagement confirmationManagement;
@Mock
private DdiSecurityProperties securityProperties;
@@ -156,7 +160,7 @@ public class AmqpControllerAuthenticationTest {
amqpMessageHandlerService = new AmqpMessageHandlerService(rabbitTemplate,
mock(AmqpMessageDispatcherService.class), controllerManagement, new JpaEntityFactory(),
systemSecurityContext, tenantConfigurationManagementMock);
systemSecurityContext, tenantConfigurationManagementMock, confirmationManagement);
amqpAuthenticationMessageHandlerService = new AmqpAuthenticationMessageHandler(rabbitTemplate,
authenticationManager, artifactManagementMock, cacheMock, hostnameResolverMock, controllerManagement,

View File

@@ -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.DmfActionUpdateStatus;
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.DmfDownloadResponse;
import org.eclipse.hawkbit.dmf.json.model.DmfUpdateMode;
import org.eclipse.hawkbit.repository.ArtifactManagement;
import org.eclipse.hawkbit.repository.ConfirmationManagement;
import org.eclipse.hawkbit.repository.ControllerManagement;
import org.eclipse.hawkbit.repository.EntityFactory;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.UpdateMode;
import org.eclipse.hawkbit.repository.builder.ActionStatusBuilder;
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.model.JpaActionStatus;
import org.eclipse.hawkbit.repository.jpa.model.helper.SecurityTokenGeneratorHolder;
@@ -110,6 +113,9 @@ public class AmqpMessageHandlerServiceTest {
@Mock
private ControllerManagement controllerManagementMock;
@Mock
private ConfirmationManagement confirmationManagementMock;
@Mock
private EntityFactory entityFactoryMock;
@@ -146,6 +152,12 @@ public class AmqpMessageHandlerServiceTest {
@Captor
private ArgumentCaptor<String> targetIdCaptor;
@Captor
private ArgumentCaptor<String> initiatorCaptor;
@Captor
private ArgumentCaptor<String> remarkCaptor;
@Captor
private ArgumentCaptor<String> targetNameCaptor;
@@ -169,7 +181,8 @@ public class AmqpMessageHandlerServiceTest {
final SystemSecurityContext systemSecurityContext = new SystemSecurityContext(tenantAware);
amqpMessageHandlerService = new AmqpMessageHandlerService(rabbitTemplate, amqpMessageDispatcherServiceMock,
controllerManagementMock, entityFactoryMock, systemSecurityContext, tenantConfigurationManagement);
controllerManagementMock, entityFactoryMock, systemSecurityContext, tenantConfigurationManagement,
confirmationManagementMock);
amqpAuthenticationMessageHandlerService = new AmqpAuthenticationMessageHandler(rabbitTemplate,
authenticationManagerMock, artifactManagementMock, downloadIdCache, hostnameResolverMock,
controllerManagementMock, tenantAwareMock);
@@ -230,6 +243,16 @@ public class AmqpMessageHandlerServiceTest {
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
@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() {
@@ -556,6 +579,7 @@ public class AmqpMessageHandlerServiceTest {
final ActionStatusCreate create = mock(ActionStatusCreate.class);
when(builder.create(22L)).thenReturn(create);
when(create.status(any())).thenReturn(create);
when(create.messages(any())).thenReturn(create);
when(entityFactoryMock.actionStatus()).thenReturn(builder);
// for the test the same action can be used
when(controllerManagementMock.findActiveActionWithHighestWeight(any())).thenReturn(Optional.of(action));
@@ -572,7 +596,7 @@ public class AmqpMessageHandlerServiceTest {
final ArgumentCaptor<Target> targetCaptor = ArgumentCaptor.forClass(Target.class);
verify(amqpMessageDispatcherServiceMock, times(1)).sendUpdateMessageToTarget(actionPropertiesCaptor.capture(),
targetCaptor.capture(), any(Map.class));
targetCaptor.capture(), any(Map.class));
final ActionProperties actionProperties = actionPropertiesCaptor.getValue();
assertThat(actionProperties).isNotNull();
assertThat(actionProperties.getTenant()).as("event has tenant").isEqualTo("DEFAULT");
@@ -616,6 +640,56 @@ public class AmqpMessageHandlerServiceTest {
.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) {
return createActionUpdateStatus(status, 2L);
}

View File

@@ -12,7 +12,6 @@ import static org.assertj.core.api.Assertions.assertThat;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
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.DmfActionUpdateStatus;
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.DmfDownloadAndUpdateRequest;
import org.eclipse.hawkbit.dmf.json.model.DmfMetadata;
@@ -129,7 +129,13 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt
testdataFactory.addSoftwareModuleMetadata(distributionSet);
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,
@@ -138,7 +144,11 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt
registerAndAssertTargetWithExistingTenant(controllerId, 1, expectedStatus, CREATED_BY);
final DistributionSetAssignmentResult assignmentResult = assignDistributionSet(assignDs, controllerId);
assertDownloadAndInstallMessage(expectedSoftwareModulesInMessage, controllerId);
if (isConfirmationFlowEnabled()) {
assertConfirmMessage(expectedSoftwareModulesInMessage, controllerId);
} else {
assertDownloadAndInstallMessage(expectedSoftwareModulesInMessage, controllerId);
}
return assignmentResult;
}
@@ -452,4 +462,24 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt
softwareModules.forEach(dmfModule -> assertThat(dmfModule.getMetadata()).containsExactly(
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());
}
}

View File

@@ -466,7 +466,7 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer
private Rollout createAndStartRollout(final DistributionSet ds, final String filterQuery, final Integer weight) {
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.handleRollouts();
return rollout;
@@ -695,4 +695,28 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer
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();
}
}

View File

@@ -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.TargetCreatedEvent;
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.jpa.model.JpaDistributionSet;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
@@ -966,6 +968,26 @@ class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServiceIntegr
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() {
return Integer
.parseInt(Objects.requireNonNull(getRabbitAdmin().getQueueProperties(amqpProperties.getReceiverQueue()))
@@ -993,4 +1015,142 @@ class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServiceIntegr
assertThat(node.has("actionId")).isTrue();
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);
}
}

View File

@@ -33,6 +33,11 @@ public enum EventTopic {
*/
UPDATE_ATTRIBUTES,
/**
* Topic when updating auto-confirmation state.
*/
UPDATE_AUTO_CONFIRM,
/**
* 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.
*/
BATCH_DOWNLOAD_AND_INSTALL
BATCH_DOWNLOAD_AND_INSTALL,
/**
* Topic when confirmation of an action is requested.
*/
CONFIRM
}

View File

@@ -64,5 +64,15 @@ public enum DmfActionStatus {
/**
* Action has been downloaded for this target.
*/
DOWNLOADED
DOWNLOADED,
/**
* Action is confirmed by the target.
*/
CONFIRMED,
/**
* Action has been denied by the target.
*/
DENIED
}

View File

@@ -34,10 +34,10 @@ public class DmfActionUpdateStatus {
private Long softwareModuleId;
@JsonProperty
private Integer code;
private List<String> message;
@JsonProperty
private List<String> message;
private Integer code;
public DmfActionUpdateStatus(@JsonProperty(value = "actionId", required = true) final Long actionId,
@JsonProperty(value = "actionStatus", required = true) final DmfActionStatus actionStatus) {
@@ -99,4 +99,8 @@ public class DmfActionUpdateStatus {
return message.addAll(messages);
}
public void setCode(final int code) {
this.code = code;
}
}

View File

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

View File

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

View File

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

View File

@@ -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.Status;
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.SoftwareModuleMetadata;
import org.eclipse.hawkbit.repository.model.Target;
@@ -508,4 +509,28 @@ public interface ControllerManagement {
*/
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
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);
}

View File

@@ -449,8 +449,8 @@ public interface DeploymentManagement {
Page<Action> findInActiveActionsByTarget(@NotNull Pageable pageable, @NotEmpty String controllerId);
/**
* Retrieves active {@link Action}s with highest weight that are assigned to
* a {@link Target}.
* Retrieves active {@link Action}s with highest weight that are assigned to a
* {@link Target}.
*
* @param controllerId
* identifies the target to retrieve the action from
@@ -522,6 +522,8 @@ public interface DeploymentManagement {
*
* @param rolloutId
* the rollout the actions belong to
* @param distributionSetId
* to assign
* @param rolloutGroupParentId
* the parent rollout group the actions should reference. null
* references the first group

View File

@@ -108,27 +108,28 @@ public interface RolloutManagement {
/**
* Persists a new rollout entity. The filter within the
* {@link Rollout#getTargetFilterQuery()} is used to retrieve the targets
* which are effected by this rollout to create. The amount of groups will
* be defined as equally sized.
* {@link Rollout#getTargetFilterQuery()} is used to retrieve the targets which
* are effected by this rollout to create. The amount of groups will be defined
* as equally sized.
*
* The rollout is not started. Only the preparation of the rollout is done,
* creating and persisting all the necessary groups. The Rollout and the
* groups are persisted in {@link RolloutStatus#CREATING} and
* creating and persisting all the necessary groups. The Rollout and the groups
* are persisted in {@link RolloutStatus#CREATING} and
* {@link RolloutGroupStatus#CREATING}.
*
* The RolloutScheduler will start to assign targets to the groups. Once all
* targets have been assigned to the groups, the rollout status is changed
* to {@link RolloutStatus#READY} so it can be started with
* {@link #start(Rollout)}.
* targets have been assigned to the groups, the rollout status is changed to
* {@link RolloutStatus#READY} so it can be started with .
*
* @param create
* the rollout entity to create
* @param amountGroup
* 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
* the rolloutgroup conditions and actions which should be
* applied for each {@link RolloutGroup}
* the rolloutgroup conditions and actions which should be applied
* for each {@link RolloutGroup}
* @return the persisted rollout.
*
* @throws EntityNotFoundException
@@ -140,22 +141,23 @@ public interface RolloutManagement {
* exceeded.
*/
@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
* {@link Rollout#getTargetFilterQuery()} is used to filter the targets
* which are affected by this rollout. The given groups will be used to
* create the groups.
* {@link Rollout#getTargetFilterQuery()} is used to filter the targets which
* are affected by this rollout. The given groups will be used to create the
* groups.
*
* The rollout is not started. Only the preparation of the rollout is done,
* creating and persisting all the necessary groups. The Rollout and the
* groups are persisted in {@link RolloutStatus#CREATING} and
* creating and persisting all the necessary groups. The Rollout and the groups
* are persisted in {@link RolloutStatus#CREATING} and
* {@link RolloutGroupStatus#CREATING}.
*
* The RolloutScheduler will start to assign targets to the groups. Once all
* targets have been assigned to the groups, the rollout status is changed
* to {@link RolloutStatus#READY} so it can be started with
* targets have been assigned to the groups, the rollout status is changed to
* {@link RolloutStatus#READY} so it can be started with
* {@link #start(Rollout)}.
*
* @param rollout
@@ -163,9 +165,9 @@ public interface RolloutManagement {
* @param groups
* optional definition of groups
* @param conditions
* the rollout group conditions and actions which should be
* applied for each {@link RolloutGroup} if not defined by the
* RolloutGroup itself
* the rollout group conditions and actions which should be applied
* for each {@link RolloutGroup} if not defined by the RolloutGroup
* itself
* @return the persisted rollout.
*
* @throws EntityNotFoundException

View File

@@ -29,6 +29,8 @@ public class AutoAssignDistributionSetUpdate {
@Max(Action.WEIGHT_MAX)
private Integer weight;
private Boolean confirmationRequired;
/**
* Constructor
*
@@ -75,6 +77,19 @@ public class AutoAssignDistributionSetUpdate {
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() {
return dsId;
}
@@ -87,6 +102,10 @@ public class AutoAssignDistributionSetUpdate {
return weight;
}
public Boolean isConfirmationRequired() {
return confirmationRequired;
}
public long getTargetFilterId() {
return targetFilterId;
}

View File

@@ -63,6 +63,14 @@ public interface RolloutGroupCreate {
*/
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
*/

View File

@@ -83,6 +83,17 @@ public interface TargetFilterQueryCreate {
*/
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
*/

View File

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

View File

@@ -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
}
}

View File

@@ -259,7 +259,12 @@ public interface Action extends TenantAwareBaseEntity {
* Action has been downloaded by the target and waiting for update to
* 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.
*/
boolean isMaintenanceWindowAvailable();
/**
* Checks if the action is waiting for confirmation.
* @return true if the action is waiting for confirmation, else false
*/
boolean isWaitingConfirmation();
}

View File

@@ -8,6 +8,8 @@
*/
package org.eclipse.hawkbit.repository.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import java.io.Serializable;
/**
@@ -22,6 +24,8 @@ public class ActionProperties implements Serializable {
private String tenant;
private boolean maintenanceWindowAvailable;
private Action.Status status;
public ActionProperties() {
}
@@ -35,6 +39,7 @@ public class ActionProperties implements Serializable {
this.actionType = action.getActionType();
this.tenant = action.getTenant();
this.maintenanceWindowAvailable = action.isMaintenanceWindowAvailable();
this.status = action.getStatus();
}
public void setId(final Long id) {
@@ -68,4 +73,13 @@ public class ActionProperties implements Serializable {
public void setActionType(final Action.ActionType actionType) {
this.actionType = actionType;
}
public Action.Status getStatus() {
return status;
}
@JsonIgnore
public boolean isWaitingConfirmation() {
return status == Action.Status.WAIT_FOR_CONFIRMATION;
}
}

View File

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

View File

@@ -25,8 +25,8 @@ public class DeploymentRequest {
private final TargetWithActionType targetWithActionType;
/**
* Constructor that also accepts maintenance schedule parameters and checks
* for validity of the specified maintenance schedule.
* Constructor that also accepts maintenance schedule parameters and checks for
* validity of the specified maintenance schedule.
*
* @param controllerId
* for which the action is created.
@@ -48,18 +48,30 @@ public class DeploymentRequest {
* window, for example 00:30:00 for 30 minutes
* @param maintenanceWindowTimeZone
* 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
* start time of a maintenance window calculated based on the
* cron expression is relative to this time zone.
* example +02:00 for CET summer time and +00:00 for UTC. The start
* time of a maintenance window calculated based on the cron
* 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
* if the parameters do not define a valid maintenance schedule.
*/
public DeploymentRequest(final String controllerId, final Long distributionSetId, final ActionType actionType,
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,
maintenanceSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone);
maintenanceSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone, confirmationRequired);
this.distributionSetId = distributionSetId;
}
@@ -78,11 +90,11 @@ public class DeploymentRequest {
@Override
public String toString() {
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.getForceTime(), targetWithActionType.getWeight(),
targetWithActionType.getMaintenanceSchedule(), targetWithActionType.getMaintenanceWindowDuration(),
targetWithActionType.getMaintenanceWindowTimeZone());
targetWithActionType.getMaintenanceWindowTimeZone(), targetWithActionType.isConfirmationRequired());
}
@Override

View File

@@ -24,6 +24,7 @@ public class DeploymentRequestBuilder {
private String maintenanceSchedule;
private String maintenanceWindowDuration;
private String maintenanceWindowTimeZone;
private boolean confirmationRequired;
/**
* Create a builder for a target distribution set assignment with the
@@ -100,6 +101,18 @@ public class DeploymentRequestBuilder {
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
*
@@ -107,7 +120,7 @@ public class DeploymentRequestBuilder {
*/
public DeploymentRequest build() {
return new DeploymentRequest(controllerId, distributionSetId, actionType, forceTime, weight,
maintenanceSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone);
maintenanceSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone, confirmationRequired);
}
}

View File

@@ -109,6 +109,12 @@ public interface RolloutGroup extends NamedEntity {
*/
float getTargetPercentage();
/**
* @return if a confirmation is required for the resulting actions (considered
* with confirmation flow active only)
*/
boolean isConfirmationRequired();
/**
* Rollout group state machine.
*

View File

@@ -98,6 +98,14 @@ public interface Target extends NamedEntity {
*/
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
* {@link #getControllerAttributes()}.

View File

@@ -81,4 +81,10 @@ public interface TargetFilterQuery extends TenantAwareBaseEntity {
* @return the user that triggered the auto assignment
*/
String getAutoAssignInitiatedBy();
/**
* @return if confirmation is required for configured auto assignment
* (considered with confirmation flow active)
*/
boolean isConfirmationRequired();
}

View File

@@ -30,6 +30,7 @@ public class TargetWithActionType {
private String maintenanceSchedule;
private String maintenanceWindowDuration;
private String maintenanceWindowTimeZone;
private final boolean confirmationRequired;
/**
* Constructor that uses {@link ActionType#FORCED}
@@ -38,7 +39,7 @@ public class TargetWithActionType {
* ID if the controller
*/
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
* specified for the action.
* @param forceTime
* after that point in time the action is exposed as forced in
* case the type is {@link ActionType#TIMEFORCED}
* after that point in time the action is exposed as forced in case
* the type is {@link ActionType#TIMEFORCED}
* @param weight
* 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,
final Integer weight) {
public TargetWithActionType(
final String controllerId, final ActionType actionType, final long forceTime,
final Integer weight, final boolean confirmationRequired) {
this.controllerId = controllerId;
this.actionType = actionType != null ? actionType : ActionType.FORCED;
this.forceTime = forceTime;
this.weight = weight;
this.confirmationRequired = confirmationRequired;
}
/**
@@ -93,8 +99,8 @@ public class TargetWithActionType {
*/
public TargetWithActionType(final String controllerId, final ActionType actionType, final long forceTime,
final Integer weight, final String maintenanceSchedule, final String maintenanceWindowDuration,
final String maintenanceWindowTimeZone) {
this(controllerId, actionType, forceTime, weight);
final String maintenanceWindowTimeZone, final boolean confirmationRequired) {
this(controllerId, actionType, forceTime, weight, confirmationRequired);
this.maintenanceSchedule = maintenanceSchedule;
this.maintenanceWindowDuration = maintenanceWindowDuration;
@@ -152,18 +158,27 @@ public class TargetWithActionType {
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
public String toString() {
return "TargetWithActionType [controllerId=" + controllerId + ", actionType=" + getActionType() + ", forceTime="
+ getForceTime() + ", weight=" + getWeight() + ", maintenanceSchedule=" + getMaintenanceSchedule()
+ ", maintenanceWindowDuration=" + getMaintenanceWindowDuration() + ", maintenanceWindowTimeZone="
+ getMaintenanceWindowTimeZone() + "]";
+ getMaintenanceWindowTimeZone() + ", confirmationRequired=" + isConfirmationRequired() + "]";
}
@Override
public int hashCode() {
return Objects.hash(actionType, controllerId, forceTime, weight, maintenanceSchedule, maintenanceWindowDuration,
maintenanceWindowTimeZone);
return Objects.hash(actionType, controllerId, forceTime, weight, confirmationRequired, maintenanceSchedule,
maintenanceWindowDuration, maintenanceWindowTimeZone);
}
@SuppressWarnings("squid:S1067")
@@ -181,8 +196,10 @@ public class TargetWithActionType {
final TargetWithActionType other = (TargetWithActionType) obj;
return Objects.equals(actionType, other.actionType) && Objects.equals(controllerId, other.controllerId)
&& Objects.equals(forceTime, other.forceTime) && Objects.equals(weight, other.weight)
&& Objects.equals(confirmationRequired, other.confirmationRequired)
&& Objects.equals(maintenanceSchedule, other.maintenanceSchedule)
&& Objects.equals(maintenanceWindowDuration, other.maintenanceWindowDuration)
&& Objects.equals(maintenanceWindowTimeZone, other.maintenanceWindowTimeZone);
}
}

View File

@@ -161,6 +161,7 @@ public class TotalTargetCountStatus {
case RUNNING:
case WARNING:
case DOWNLOAD:
case WAIT_FOR_CONFIRMATION:
case CANCELING:
return Status.RUNNING;
case DOWNLOADED:

View File

@@ -152,6 +152,11 @@ public class TenantConfigurationProperties {
*/
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 defaultValue = "";
private Class<?> dataType = String.class;

View File

@@ -6,9 +6,10 @@
* which accompanies this distribution, and is available at
* 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.USER_CONFIRMATION_ENABLED;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.security.SystemSecurityContext;
@@ -50,4 +51,14 @@ public final class TenantConfigHelper {
return systemSecurityContext.runAsSystem(() -> tenantConfigurationManagement
.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());
}
}

View File

@@ -23,6 +23,7 @@ public abstract class AbstractRolloutGroupCreate<T> extends AbstractNamedEntityB
protected String targetFilterQuery;
protected Float targetPercentage;
protected RolloutGroupConditions conditions;
protected boolean confirmationRequired;
public T targetFilterQuery(final String targetFilterQuery) {
this.targetFilterQuery = StringUtils.trimWhitespace(targetFilterQuery);
@@ -39,4 +40,9 @@ public abstract class AbstractRolloutGroupCreate<T> extends AbstractNamedEntityB
return (T) this;
}
public T confirmationRequired(final boolean confirmationRequired) {
this.confirmationRequired = confirmationRequired;
return (T) this;
}
}

View File

@@ -39,6 +39,8 @@ public abstract class AbstractTargetFilterQueryUpdateCreate<T> extends AbstractB
@Min(Action.WEIGHT_MIN)
@Max(Action.WEIGHT_MAX)
protected Integer weight;
protected Boolean confirmationRequired;
/**
* 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() {
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);
}
}

View File

@@ -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.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

View File

@@ -57,11 +57,13 @@ public abstract class AbstractDsAssignmentStrategy {
private final ActionStatusRepository actionStatusRepository;
private final QuotaManagement quotaManagement;
private final BooleanSupplier multiAssignmentsConfig;
private final BooleanSupplier confirmationFlowConfig;
AbstractDsAssignmentStrategy(final TargetRepository targetRepository,
final AfterTransactionCommitExecutor afterCommit, final EventPublisherHolder eventPublisherHolder,
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.afterCommit = afterCommit;
this.eventPublisherHolder = eventPublisherHolder;
@@ -69,6 +71,7 @@ public abstract class AbstractDsAssignmentStrategy {
this.actionStatusRepository = actionStatusRepository;
this.quotaManagement = quotaManagement;
this.multiAssignmentsConfig = multiAssignmentsConfig;
this.confirmationFlowConfig = confirmationFlowConfig;
}
/**
@@ -276,4 +279,8 @@ public abstract class AbstractDsAssignmentStrategy {
protected boolean isMultiAssignmentsEnabled() {
return multiAssignmentsConfig.getAsBoolean();
}
protected boolean isConfirmationFlowEnabled() {
return confirmationFlowConfig.getAsBoolean();
}
}

View File

@@ -233,8 +233,8 @@ public interface ActionRepository extends BaseEntityRepository<JpaAction, Long>,
/**
* Retrieves all {@link Action}s of a specific target and given active flag
* ordered by action ID. Loads also the lazy
* {@link Action#getDistributionSet()} field.
* ordered by action ID. Loads also the lazy {@link Action#getDistributionSet()}
* field.
*
* @param pageable
* page parameters
@@ -251,9 +251,8 @@ public interface ActionRepository extends BaseEntityRepository<JpaAction, Long>,
@Param("active") boolean active);
/**
* Switches the status of actions from one specific status into another,
* only if the actions are in a specific status. This should be a atomar
* operation.
* Switches the status of actions from one specific status into another, only if
* the actions are in a specific status. This should be a atomar operation.
*
* @param statusToSet
* 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,
@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,
@@ -490,8 +504,8 @@ public interface ActionRepository extends BaseEntityRepository<JpaAction, Long>,
boolean existsByRolloutIdAndStatusNotIn(@Param("rolloutId") Long rolloutId, @Param("status") Status status);
/**
* Retrieving all actions referring to a given rollout with a specific
* action as parent reference and a specific status.
* Retrieving all actions referring to a given rollout with a specific action as
* parent reference and a specific status.
*
* 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
* 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,
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
* 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,
Status actionStatus);

View File

@@ -14,24 +14,44 @@ import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.hawkbit.repository.QuotaManagement;
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.ActionStatus;
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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
*/
public class JpaActionManagement {
private static final Logger LOG = LoggerFactory.getLogger(JpaActionManagement.class);
protected final ActionRepository actionRepository;
protected final ActionStatusRepository actionStatusRepository;
protected final QuotaManagement quotaManagement;
protected final RepositoryProperties repositoryProperties;
protected JpaActionManagement(final ActionRepository actionRepository,
final ActionStatusRepository actionStatusRepository, final QuotaManagement quotaManagement,
final RepositoryProperties repositoryProperties) {
this.actionRepository = actionRepository;
this.actionStatusRepository = actionStatusRepository;
this.quotaManagement = quotaManagement;
this.repositoryProperties = repositoryProperties;
}
protected List<Action> findActiveActionsWithHighestWeightConsideringDefault(final String controllerId,
final int maxActionCount) {
if (!actionRepository.activeActionExistsForControllerId(controllerId)) {
@@ -50,8 +70,90 @@ public class JpaActionManagement {
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) {
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);
}
}

View File

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

View File

@@ -8,7 +8,6 @@
*/
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.FINISHED;
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.NotNull;
import org.eclipse.hawkbit.repository.ConfirmationManagement;
import org.eclipse.hawkbit.repository.ControllerManagement;
import org.eclipse.hawkbit.repository.EntityFactory;
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.Status;
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.SoftwareModule;
import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata;
@@ -125,12 +126,6 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
@Autowired
private SoftwareModuleRepository softwareModuleRepository;
@Autowired
private ActionStatusRepository actionStatusRepository;
@Autowired
private QuotaManagement quotaManagement;
@Autowired
private TenantConfigurationManagement tenantConfigurationManagement;
@@ -155,9 +150,13 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
@Autowired
private TenantAware tenantAware;
@Autowired
private ConfirmationManagement confirmationManagement;
public JpaControllerManagement(final ScheduledExecutorService executorService,
final RepositoryProperties repositoryProperties, final ActionRepository actionRepository) {
super(actionRepository, repositoryProperties);
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
final QuotaManagement quotaManagement, final RepositoryProperties repositoryProperties) {
super(actionRepository, actionStatusRepository, quotaManagement, repositoryProperties);
if (!repositoryProperties.isEagerPollPersistence()) {
executorService.scheduleWithFixedDelay(this::flushUpdateQueue,
@@ -565,11 +564,6 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
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) {
// in case of successful cancellation we also report the success at
// the canceled action itself.
@@ -582,88 +576,41 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
@Transactional(isolation = Isolation.READ_COMMITTED)
@Retryable(include = {
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public Action addUpdateActionStatus(final ActionStatusCreate c) {
final JpaActionStatusCreate create = (JpaActionStatusCreate) c;
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;
public Action addUpdateActionStatus(final ActionStatusCreate statusCreate) {
return addActionStatus((JpaActionStatusCreate) statusCreate);
}
/**
* 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.
*/
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()) {
@Override
protected void onActionStatusUpdate(final Action.Status updatedActionStatus, final JpaAction action) {
switch (updatedActionStatus) {
case ERROR:
final JpaTarget target = (JpaTarget) action.getTarget();
target.setUpdateStatus(TargetUpdateStatus.ERROR);
handleErrorOnAction(action, target);
break;
case FINISHED:
controllerId = handleFinishedAndStoreInTargetStatus(action);
handleFinishedAndStoreInTargetStatus(action).ifPresent(this::requestControllerAttributes);
break;
case DOWNLOADED:
controllerId = handleDownloadedActionStatus(action);
handleDownloadedActionStatus(action).ifPresent(this::requestControllerAttributes);
break;
default:
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)) {
return null;
return Optional.empty();
}
final JpaTarget target = (JpaTarget) action.getTarget();
@@ -672,7 +619,7 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
target.setUpdateStatus(TargetUpdateStatus.IN_SYNC);
targetRepository.save(target);
return target.getControllerId();
return Optional.of(target.getControllerId());
}
private void requestControllerAttributes(final String controllerId) {
@@ -695,12 +642,16 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
targetRepository.save(mergedTarget);
}
private void assertActionStatusQuota(final JpaAction action) {
QuotaHelper.assertAssignmentQuota(action.getId(), 1, quotaManagement.getMaxStatusEntriesPerAction(),
ActionStatus.class, Action.class, actionStatusRepository::countByActionId);
}
private String handleFinishedAndStoreInTargetStatus(final JpaAction action) {
/**
* Handles the case where the {@link Action.Status#FINISHED} 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> handleFinishedAndStoreInTargetStatus(final JpaAction action) {
final JpaTarget target = (JpaTarget) action.getTarget();
action.setActive(false);
action.setStatus(Status.FINISHED);
@@ -727,7 +678,7 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
targetRepository.save(target);
entityManager.detach(ds);
return target.getControllerId();
return Optional.of(target.getControllerId());
}
@Override
@@ -884,11 +835,6 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
return actionStatusRepository.save(statusMessage);
}
private JpaAction getActionAndThrowExceptionIfNotFound(final Long actionId) {
return actionRepository.findById(actionId)
.orElseThrow(() -> new EntityNotFoundException(Action.class, actionId));
}
@Override
public Optional<Target> getByControllerId(final String controllerId) {
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) {
afterCommit.afterCommit(() -> eventPublisherHolder.getEventPublisher().publishEvent(
new CancelTargetAssignmentEvent(action, eventPublisherHolder.getApplicationId())));

View File

@@ -16,6 +16,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
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.utils.DeploymentHelper;
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.model.Action;
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.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.hawkbit.utils.TenantConfigHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.ConcurrencyFailureException;
@@ -138,14 +139,12 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
private final DistributionSetManagement distributionSetManagement;
private final DistributionSetRepository distributionSetRepository;
private final TargetRepository targetRepository;
private final ActionStatusRepository actionStatusRepository;
private final AuditorAware<String> auditorProvider;
private final VirtualPropertyReplacer virtualPropertyReplacer;
private final PlatformTransactionManager txManager;
private final OnlineDsAssignmentStrategy onlineDsAssignmentStrategy;
private final OfflineDsAssignmentStrategy offlineDsAssignmentStrategy;
private final TenantConfigurationManagement tenantConfigurationManagement;
private final QuotaManagement quotaManagement;
private final SystemSecurityContext systemSecurityContext;
private final TenantAware tenantAware;
private final Database database;
@@ -160,26 +159,25 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
final TenantConfigurationManagement tenantConfigurationManagement, final QuotaManagement quotaManagement,
final SystemSecurityContext systemSecurityContext, final TenantAware tenantAware, final Database database,
final RepositoryProperties repositoryProperties) {
super(actionRepository, repositoryProperties);
super(actionRepository, actionStatusRepository, quotaManagement, repositoryProperties);
this.entityManager = entityManager;
this.distributionSetRepository = distributionSetRepository;
this.distributionSetManagement = distributionSetManagement;
this.targetRepository = targetRepository;
this.actionStatusRepository = actionStatusRepository;
this.auditorProvider = auditorProvider;
this.virtualPropertyReplacer = virtualPropertyReplacer;
this.txManager = txManager;
onlineDsAssignmentStrategy = new OnlineDsAssignmentStrategy(targetRepository, afterCommit, eventPublisherHolder,
actionRepository, actionStatusRepository, quotaManagement, this::isMultiAssignmentsEnabled);
actionRepository, actionStatusRepository, quotaManagement, this::isMultiAssignmentsEnabled,
this::isConfirmationFlowEnabled);
offlineDsAssignmentStrategy = new OfflineDsAssignmentStrategy(targetRepository, afterCommit,
eventPublisherHolder, actionRepository, actionStatusRepository, quotaManagement,
this::isMultiAssignmentsEnabled);
this::isMultiAssignmentsEnabled, this::isConfirmationFlowEnabled);
this.tenantConfigurationManagement = tenantConfigurationManagement;
this.quotaManagement = quotaManagement;
this.systemSecurityContext = systemSecurityContext;
this.tenantAware = tenantAware;
this.database = database;
retryTemplate = createRetryTemplate();
this.retryTemplate = createRetryTemplate();
}
@Override
@@ -393,7 +391,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
targetEntitiesIdsChunks.forEach(this::cancelInactiveScheduledActionsForTargets);
setAssignedDistributionSetAndTargetUpdateStatus(assignmentStrategy, distributionSetEntity,
targetEntitiesIdsChunks);
final List<JpaAction> assignedActions = createActions(initiatedBy, targetsWithActionType, targetEntities,
final Map<TargetWithActionType, JpaAction> assignedActions = createActions(initiatedBy, targetsWithActionType, targetEntities,
assignmentStrategy, distributionSetEntity);
// create initial action status when action is created so we remember
// 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);
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);
}
private List<JpaAction> createActions(final String initiatedBy,
private Map<TargetWithActionType, JpaAction> createActions(final String initiatedBy,
final Collection<TargetWithActionType> targetsWithActionType, final List<JpaTarget> targets,
final AbstractDsAssignmentStrategy assignmentStrategy, final JpaDistributionSet set) {
return targetsWithActionType.stream()
.map(twt -> assignmentStrategy.createTargetAction(initiatedBy, twt, targets, set))
.filter(Objects::nonNull).map(actionRepository::save).collect(Collectors.toList());
final Map<TargetWithActionType, JpaAction> persistedActions = new LinkedHashMap<>();
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) {
actionStatusRepository
.saveAll(actions.stream().map(action -> assignmentStrategy.createActionStatus(action, actionMessage))
.collect(Collectors.toList()));
actionStatusRepository.saveAll(actions.entrySet().stream().map(entry -> {
final JpaAction action = entry.getValue();
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,
@@ -643,15 +683,15 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
if (!isMultiAssignmentsEnabled()) {
closeOrCancelOpenDeviceActions(actions);
}
final List<JpaAction> savedActions = activateActions(actions);
setInitialActionStatus(savedActions);
final List<JpaAction> savedActions = activateActionsOfRolloutGroup(actions);
setInitialActionStatusOfRolloutGroup(savedActions);
setAssignmentOnTargets(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)
.collect(Collectors.toList());
.collect(Collectors.toList());
if (isActionsAutocloseEnabled()) {
onlineDsAssignmentStrategy.closeObsoleteUpdateActions(targetIds);
} 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 -> {
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);
});
return actionRepository.saveAll(actions);
@@ -678,14 +724,6 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
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) {
final JpaActionStatus actionStatus = new JpaActionStatus();
actionStatus.setAction(action);
@@ -745,7 +783,6 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
public List<Action> findActiveActionsWithHighestWeight(final String controllerId, final int maxActionCount) {
return findActiveActionsWithHighestWeightConsideringDefault(controllerId, maxActionCount);
}
@Override
public int getWeightConsideringDefault(final Action action) {
return super.getWeightConsideringDefault(action);
@@ -967,6 +1004,11 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
.isMultiAssignmentsEnabled();
}
private boolean isConfirmationFlowEnabled() {
return TenantConfigHelper.usingContext(systemSecurityContext, tenantConfigurationManagement)
.isConfirmationFlowEnabled();
}
private <T extends Serializable> T getConfigValue(final String key, final Class<T> valueType) {
return systemSecurityContext
.runAsSystem(() -> tenantConfigurationManagement.getConfigurationValue(key, valueType).getValue());
@@ -1002,5 +1044,4 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
});
}
}
}

View File

@@ -71,6 +71,7 @@ import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.hawkbit.utils.TenantConfigHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -193,10 +194,11 @@ public class JpaRolloutManagement implements RolloutManagement {
@Transactional
@Retryable(include = {
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);
final JpaRollout savedRollout = createRollout((JpaRollout) rollout.build());
return createRolloutGroups(amountGroup, conditions, savedRollout);
return createRolloutGroups(amountGroup, conditions, savedRollout, confirmationRequired);
}
@Override
@@ -222,7 +224,7 @@ public class JpaRolloutManagement implements RolloutManagement {
}
private Rollout createRolloutGroups(final int amountOfGroups, final RolloutGroupConditions conditions,
final JpaRollout rollout) {
final JpaRollout rollout, final boolean isConfirmationRequired) {
RolloutHelper.verifyRolloutInStatus(rollout, RolloutStatus.CREATING);
RolloutHelper.verifyRolloutGroupConditions(conditions);
@@ -241,6 +243,7 @@ public class JpaRolloutManagement implements RolloutManagement {
group.setRollout(savedRollout);
group.setParent(lastSavedGroup);
group.setStatus(RolloutGroupStatus.CREATING);
group.setConfirmationRequired(isConfirmationRequired);
addSuccessAndErrorConditionsAndActions(group, conditions);
@@ -283,6 +286,7 @@ public class JpaRolloutManagement implements RolloutManagement {
group.setRollout(savedRollout);
group.setParent(lastSavedGroup);
group.setStatus(RolloutGroupStatus.CREATING);
group.setConfirmationRequired(srcGroup.isConfirmationRequired());
group.setTargetPercentage(srcGroup.getTargetPercentage());
if (srcGroup.getTargetFilterQuery() != null) {

View File

@@ -43,6 +43,7 @@ import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.hawkbit.utils.TenantConfigHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.ConcurrencyFailureException;
@@ -249,6 +250,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme
// set the new query
targetFilterQuery.setQuery(query);
});
update.getConfirmationRequired().ifPresent(targetFilterQuery::setConfirmationRequired);
return targetFilterQueryRepository.save(targetFilterQuery);
}
@@ -263,6 +265,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme
targetFilterQuery.setAutoAssignActionType(null);
targetFilterQuery.setAutoAssignWeight(null);
targetFilterQuery.setAutoAssignInitiatedBy(null);
targetFilterQuery.setConfirmationRequired(false);
} else {
WeightValidationHelper.usingContext(systemSecurityContext, tenantConfigurationManagement).validate(update);
// 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.setAutoAssignActionType(sanitizeAutoAssignActionType(update.getActionType()));
targetFilterQuery.setAutoAssignWeight(update.getWeight());
final boolean confirmationRequired = update.isConfirmationRequired() == null ? isConfirmationFlowEnabled()
: update.isConfirmationRequired();
targetFilterQuery.setConfirmationRequired(confirmationRequired);
}
return targetFilterQueryRepository.save(targetFilterQuery);
}
private boolean isConfirmationFlowEnabled() {
return TenantConfigHelper.usingContext(systemSecurityContext, tenantConfigurationManagement)
.isConfirmationFlowEnabled();
}
private static void verifyDistributionSetAndThrowExceptionIfDeleted(final DistributionSet distributionSet) {
if (distributionSet.isDeleted()) {
throw new EntityNotFoundException(DistributionSet.class, distributionSet.getId());

View File

@@ -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.TargetDeletedEvent;
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.EntityNotFoundException;
import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetCreate;
import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetUpdate;
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
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.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.TargetSpecifications;
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.DistributionSetType;
import org.eclipse.hawkbit.repository.model.MetaData;

View File

@@ -45,9 +45,10 @@ public class OfflineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
OfflineDsAssignmentStrategy(final TargetRepository targetRepository,
final AfterTransactionCommitExecutor afterCommit, final EventPublisherHolder eventPublisherHolder,
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,
quotaManagement, multiAssignmentsConfig);
quotaManagement, multiAssignmentsConfig, confirmationFlowConfig);
}
@Override

View File

@@ -48,9 +48,10 @@ public class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
OnlineDsAssignmentStrategy(final TargetRepository targetRepository,
final AfterTransactionCommitExecutor afterCommit, final EventPublisherHolder eventPublisherHolder,
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,
quotaManagement, multiAssignmentsConfig);
quotaManagement, multiAssignmentsConfig, confirmationFlowConfig);
}
@Override
@@ -130,15 +131,28 @@ public class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
final List<JpaTarget> targets, final JpaDistributionSet set) {
final JpaAction result = super.createTargetAction(initiatedBy, targetWithActionType, targets, set);
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;
}
/**
* Will be called to create the initial action status for an action
*/
@Override
public JpaActionStatus createActionStatus(final JpaAction action, final String 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;
}

View File

@@ -22,6 +22,7 @@ import org.eclipse.hawkbit.repository.ArtifactEncryptionSecretsStore;
import org.eclipse.hawkbit.repository.ArtifactEncryptionService;
import org.eclipse.hawkbit.repository.ArtifactManagement;
import org.eclipse.hawkbit.repository.BaseRepositoryTypeProvider;
import org.eclipse.hawkbit.repository.ConfirmationManagement;
import org.eclipse.hawkbit.repository.ControllerManagement;
import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.DistributionSetManagement;
@@ -742,6 +743,16 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
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.
*
@@ -750,8 +761,10 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
@Bean
@ConditionalOnMissingBean
ControllerManagement controllerManagement(final ScheduledExecutorService executorService,
final RepositoryProperties repositoryProperties, final ActionRepository actionRepository) {
return new JpaControllerManagement(executorService, repositoryProperties, actionRepository);
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
final QuotaManagement quotaManagement, final RepositoryProperties repositoryProperties) {
return new JpaControllerManagement(executorService, actionRepository, actionStatusRepository, quotaManagement,
repositoryProperties);
}
@Bean

View File

@@ -167,7 +167,7 @@ public abstract class AbstractAutoAssignExecutor implements AutoAssignExecutor {
.map(controllerId -> DeploymentManagement
.deploymentRequest(controllerId, filterQuery.getAutoAssignDistributionSet().getId())
.setActionType(autoAssignActionType).setWeight(filterQuery.getAutoAssignWeight().orElse(null))
.build())
.setConfirmationRequired(filterQuery.isConfirmationRequired()).build())
.collect(Collectors.toList());
}

View File

@@ -34,6 +34,7 @@ public class JpaRolloutGroupCreate extends AbstractRolloutGroupCreate<RolloutGro
if (conditions != null) {
addSuccessAndErrorConditionsAndActions(group, conditions);
}
group.setConfirmationRequired(confirmationRequired);
return group;
}

View File

@@ -31,11 +31,10 @@ public class JpaTargetFilterQueryCreate extends AbstractTargetFilterQueryUpdateC
@Override
public JpaTargetFilterQuery build() {
return new JpaTargetFilterQuery(name, query,
getAutoAssignDistributionSetId().map(distributionSetManagement::getValidAndComplete).orElse(null),
getAutoAssignActionType().filter(JpaTargetFilterQueryCreate::isAutoAssignActionTypeValid).orElse(null),
weight);
weight, getConfirmationRequired().orElse(false));
}
private static boolean isAutoAssignActionTypeValid(final ActionType actionType) {

View File

@@ -108,7 +108,8 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio
@ConversionValue(objectValue = "DOWNLOAD", dataValue = "7"),
@ConversionValue(objectValue = "SCHEDULED", dataValue = "8"),
@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")
@NotNull
private Status status;
@@ -236,7 +237,8 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio
public String toString() {
return "JpaAction [distributionSet=" + distributionSet.getId() + ", version=" + getOptLockRevision() + ", id="
+ getId() + ", actionType=" + getActionType() + ", weight=" + getWeight() + ", isActive=" + isActive()
+ ", createdAt=" + getCreatedAt() + ", lastModifiedAt=" + getLastModifiedAt() + "]";
+ ", createdAt=" + getCreatedAt() + ", lastModifiedAt=" + getLastModifiedAt() + ", status="
+ getStatus().name() + "]";
}
@Override
@@ -387,4 +389,8 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio
public void setLastActionStatusCode(final Integer lastActionStatusCode) {
this.lastActionStatusCode = lastActionStatusCode;
}
public boolean isWaitingConfirmation() {
return status == Status.WAIT_FOR_CONFIRMATION;
}
}

View File

@@ -74,7 +74,8 @@ public class JpaActionStatus extends AbstractJpaTenantAwareBaseEntity implements
@ConversionValue(objectValue = "DOWNLOAD", dataValue = "7"),
@ConversionValue(objectValue = "SCHEDULED", dataValue = "8"),
@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")
@NotNull
private Status status;

View File

@@ -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 + "]";
}
}

View File

@@ -117,6 +117,9 @@ public class JpaRolloutGroup extends AbstractJpaNamedEntity implements RolloutGr
@Column(name = "target_percentage")
private float targetPercentage = 100;
@Column(name = "confirmation_required")
private boolean confirmationRequired;
@Transient
private transient TotalTargetCountStatus totalTargetCountStatus;
@@ -251,6 +254,15 @@ public class JpaRolloutGroup extends AbstractJpaNamedEntity implements RolloutGr
return targetPercentage;
}
public void setConfirmationRequired(final boolean confirmationRequired) {
this.confirmationRequired = confirmationRequired;
}
@Override
public boolean isConfirmationRequired() {
return confirmationRequired;
}
public void setTargetPercentage(final float targetPercentage) {
this.targetPercentage = targetPercentage;
}
@@ -279,7 +291,8 @@ public class JpaRolloutGroup extends AbstractJpaNamedEntity implements RolloutGr
return "RolloutGroup [rollout=" + (rollout != null ? rollout.getId() : "") + ", status=" + status
+ ", rolloutTargetGroup=" + rolloutTargetGroup + ", parent=" + parent + ", finishCondition="
+ successCondition + ", finishExp=" + successConditionExp + ", errorCondition=" + errorCondition
+ ", errorExp=" + errorConditionExp + ", getName()=" + getName() + ", getId()=" + getId() + "]";
+ ", errorExp=" + errorConditionExp + ", getName()=" + getName() + ", getId()=" + getId()
+ ", isConfirmationRequired()=" + isConfirmationRequired() + "]";
}
@Override

View File

@@ -36,6 +36,8 @@ import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.MapKeyColumn;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
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.SecurityTokenGeneratorHolder;
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.PollStatus;
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)
private List<TargetMetadata> metadata;
@OneToOne(fetch = FetchType.LAZY, mappedBy = "target", orphanRemoval = true)
@PrimaryKeyJoinColumn
private JpaAutoConfirmationStatus autoConfirmationStatus;
/**
* 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
public Long getLastTargetQuery() {
return lastTargetQuery;

View File

@@ -80,6 +80,9 @@ public class JpaTargetFilterQuery extends AbstractJpaTenantAwareBaseEntity
@Column(name = "auto_assign_initiated_by", nullable = true, length = USERNAME_FIELD_LENGTH)
private String autoAssignInitiatedBy;
@Column(name = "confirmation_required")
private boolean confirmationRequired;
public JpaTargetFilterQuery() {
// Default constructor for JPA.
}
@@ -97,14 +100,17 @@ public class JpaTargetFilterQuery extends AbstractJpaTenantAwareBaseEntity
* of the {@link TargetFilterQuery}.
* @param autoAssignWeight
* of the {@link TargetFilterQuery}.
* @param confirmationRequired
* of the {@link TargetFilterQuery}.
*/
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.query = query;
this.autoAssignDistributionSet = (JpaDistributionSet) autoAssignDistributionSet;
this.autoAssignActionType = autoAssignActionType;
this.autoAssignWeight = autoAssignWeight;
this.confirmationRequired = confirmationRequired;
}
@Override
@@ -160,6 +166,15 @@ public class JpaTargetFilterQuery extends AbstractJpaTenantAwareBaseEntity
this.autoAssignInitiatedBy = autoAssignInitiatedBy;
}
@Override
public boolean isConfirmationRequired() {
return confirmationRequired;
}
public void setConfirmationRequired(final boolean confirmationRequired) {
this.confirmationRequired = confirmationRequired;
}
@Override
public void fireCreateEvent(final DescriptorEvent descriptorEvent) {
EventPublisherHolder.getInstance().getEventPublisher().publishEvent(
@@ -177,4 +192,5 @@ public class JpaTargetFilterQuery extends AbstractJpaTenantAwareBaseEntity
EventPublisherHolder.getInstance().getEventPublisher().publishEvent(new TargetFilterQueryDeletedEvent(
getTenant(), getId(), getClass(), EventPublisherHolder.getInstance().getApplicationId()));
}
}

View File

@@ -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.Rollout;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.utils.TenantConfigHelper;
/**
* Utility class to handle weight validation in Rollout, Auto Assignments, and

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,3 +1,3 @@
ALTER TABLE sp_distribution_set ADD COLUMN valid BOOLEAN;
UPDATE sp_distribution_set SET valid = 1;
UPDATE sp_distribution_set SET valid = TRUE;

View File

@@ -1,3 +1,3 @@
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;

View File

@@ -1 +1 @@
ALTER TABLE sp_target_type ALTER COLUMN name VARCHAR (128);
ALTER TABLE sp_target_type ALTER COLUMN name TYPE VARCHAR (128);

View File

@@ -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
ON sp_action_status
USING BTREE (tenant, code);

View File

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

View File

@@ -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;

View File

@@ -44,8 +44,8 @@ public class RolloutEventTest extends AbstractRemoteEntityEventTest<Rollout> {
.type("os").modules(Collections.singletonList(module.getId())));
return rolloutManagement.create(
entityFactory.rollout().create().name("exampleRollout").targetFilterQuery("controllerId==*").set(ds),
5, new RolloutGroupConditionBuilder().withDefaults()
entityFactory.rollout().create().name("exampleRollout").targetFilterQuery("controllerId==*").set(ds), 5,
false, new RolloutGroupConditionBuilder().withDefaults()
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, "10").build());
}

View File

@@ -84,8 +84,8 @@ public class RolloutGroupEventTest extends AbstractRemoteEntityEventTest<Rollout
.type("os").modules(Collections.singletonList(module.getId())));
final Rollout entity = rolloutManagement.create(
entityFactory.rollout().create().name("exampleRollout").targetFilterQuery("controllerId==*").set(ds),
5, new RolloutGroupConditionBuilder().withDefaults()
entityFactory.rollout().create().name("exampleRollout").targetFilterQuery("controllerId==*").set(ds), 5,
false, new RolloutGroupConditionBuilder().withDefaults()
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, "10").build());
return rolloutGroupManagement.findByRollout(PAGE, entity.getId()).getContent().get(0);

View File

@@ -117,7 +117,7 @@ public class ConcurrentDistributionSetInvalidationTest extends AbstractJpaIntegr
return rolloutManagement.create(entityFactory.rollout().create()
.name("verifyInvalidateDistributionSetWithLargeRolloutThrowsException").description("desc")
.targetFilterQuery("name==*").set(distributionSet).actionType(ActionType.FORCED),
quotaManagement.getMaxRolloutGroupsPerRollout(), conditions);
quotaManagement.getMaxRolloutGroupsPerRollout(), false, conditions);
}
}

View File

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

View File

@@ -796,6 +796,8 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getUpdateStatus())
.isEqualTo(TargetUpdateStatus.IN_SYNC);
assertThat(actionRepository.findById(action.getId()))
.hasValueSatisfying(a -> assertThat(a.getStatus()).isEqualTo(Status.FINISHED));
assertThat(actionStatusRepository.count()).isEqualTo(3);
assertThat(controllerManagement.findActionStatusByAction(PAGE, action.getId()).getNumberOfElements())
.isEqualTo(3);
@@ -806,7 +808,8 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
+ "configured to accept them.")
@ExpectEvents({ @Expect(type = TargetCreatedEvent.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 = TargetAssignDistributionSetEvent.class, count = 1),
@Expect(type = TargetAttributesRequestedEvent.class, count = 1),

View File

@@ -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.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 java.util.AbstractMap.SimpleEntry;
@@ -19,8 +21,10 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
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.ActionStatus;
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.DistributionSetAssignmentResult;
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.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
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.PageRequest;
import org.springframework.data.domain.Pageable;
@@ -284,7 +291,8 @@ class DeploymentManagementTest extends AbstractJpaIntegrationTest {
assignDistributionSet(cancelDs, targets).getAssignedEntity();
assertThat(deploymentManagement.countActionsAll()).isEqualTo(quotaManagement.getMaxTargetsPerAutoAssignment());
assignDistributionSet(cancelDs2, targets).getAssignedEntity();
assertThat(deploymentManagement.countActionsAll()).isEqualTo(2L * quotaManagement.getMaxTargetsPerAutoAssignment());
assertThat(deploymentManagement.countActionsAll())
.isEqualTo(2L * quotaManagement.getMaxTargetsPerAutoAssignment());
}
@Test
@@ -546,13 +554,13 @@ class DeploymentManagementTest extends AbstractJpaIntegrationTest {
final DistributionSet ds1 = testdataFactory.createDistributionSet("1");
assignDistributionSet(ds1, targets);
assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_ACTIVE, Status.RUNNING);
assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_ACTIVE, RUNNING);
// Second assignment
final DistributionSet ds2 = testdataFactory.createDistributionSet("2");
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);
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");
assignDistributionSet(ds1.getId(), targetIds, 77);
assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_ACTIVE, Status.RUNNING);
assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_ACTIVE, RUNNING);
// Second assignment
final DistributionSet ds2 = testdataFactory.createDistributionSet("Multi-assign-2");
assignDistributionSet(ds2.getId(), targetIds, 45);
assertDsExclusivelyAssignedToTargets(targets, ds2.getId(), STATE_ACTIVE, Status.RUNNING);
assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_ACTIVE, Status.RUNNING);
assertDsExclusivelyAssignedToTargets(targets, ds2.getId(), STATE_ACTIVE, RUNNING);
assertDsExclusivelyAssignedToTargets(targets, ds1.getId(), STATE_ACTIVE, RUNNING);
}
private void assertDsExclusivelyAssignedToTargets(final List<Target> targets, final long dsId, final boolean active,
@@ -651,7 +659,8 @@ class DeploymentManagementTest extends AbstractJpaIntegrationTest {
void cancelMultiAssignmentActions() {
final List<Target> targets = testdataFactory.createTargets(1);
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();
final List<DistributionSetAssignmentResult> results = deploymentManagement
@@ -672,9 +681,15 @@ class DeploymentManagementTest extends AbstractJpaIntegrationTest {
protected List<DeploymentRequest> createAssignmentRequests(final Collection<DistributionSet> distributionSets,
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<>();
distributionSets.forEach(ds -> targets.forEach(target -> deploymentRequests.add(DeploymentManagement
.deploymentRequest(target.getControllerId(), ds.getId()).setWeight(weight).build())));
distributionSets.forEach(ds -> targets.forEach(target -> deploymentRequests
.add(DeploymentManagement.deploymentRequest(target.getControllerId(), ds.getId()).setWeight(weight)
.setConfirmationRequired(confirmationRequired).build())));
return deploymentRequests;
}
@@ -713,7 +728,7 @@ class DeploymentManagementTest extends AbstractJpaIntegrationTest {
knownTargetIds.add(notExistingId);
final List<DistributionSetAssignmentResult> assignDistributionSetsResults = assignDistributionSetToTargets(
createdDs, knownTargetIds);
createdDs, knownTargetIds, false);
for (final DistributionSetAssignmentResult assignDistributionSetsResult : assignDistributionSetsResults) {
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,
final Iterable<String> targetIds) {
final Iterable<String> targetIds, final boolean confirmationRequired) {
final List<DeploymentRequest> deploymentRequests = new ArrayList<>();
for (final String controllerId : targetIds) {
deploymentRequests.add(new DeploymentRequest(controllerId, distributionSet.getId(), ActionType.FORCED, 0,
null, null, null, null));
null, null, null, null, confirmationRequired));
}
return deploymentManagement.assignDistributionSets(deploymentRequests);
}

View File

@@ -22,6 +22,7 @@ import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
@@ -31,6 +32,7 @@ import org.assertj.core.api.Assertions;
import org.assertj.core.api.Condition;
import org.awaitility.Awaitility;
import org.awaitility.Duration;
import org.eclipse.hawkbit.repository.Identifiable;
import org.eclipse.hawkbit.repository.OffsetBasedPageRequest;
import org.eclipse.hawkbit.repository.builder.RolloutCreate;
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.junit.jupiter.api.BeforeEach;
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.PageRequest;
import org.springframework.data.domain.Pageable;
@@ -139,6 +144,42 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
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
@Description("Verifies that a running action is auto canceled by a rollout which assigns another distribution-set.")
void rolloutAssignsNewDistributionSetAndAutoCloseActiveActions() {
@@ -1369,7 +1410,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
.targetFilterQuery("controllerId==" + targetPrefixName + "-*").set(distributionSet);
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> rolloutManagement.create(rollout, amountGroups, conditions));
.isThrownBy(() -> rolloutManagement.create(rollout, amountGroups, false, conditions));
}
@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
@Description("Verify rollout creation fails if group definition does not address all targets")
void createRolloutWithGroupsNotMatchingTargets() {
@@ -1593,7 +1745,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
final RolloutCreate rollout = generateTargetsAndRollout(rolloutName, targets);
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> rolloutManagement.create(rollout, maxGroups + 1, conditions))
.isThrownBy(() -> rolloutManagement.create(rollout, maxGroups + 1, false, conditions))
.withMessageContaining("not be greater than " + maxGroups);
}
@@ -1615,7 +1767,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
final RolloutCreate rolloutToCreate = entityFactory.rollout().create().name(rolloutName)
.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());
assertThat(myRollout.getStatus()).isEqualTo(RolloutStatus.CREATING);
@@ -1787,7 +1939,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
.name(prefixRolloutRunning + "-testRollout").targetFilterQuery("name==" + randomString + "*")
.set(testDs);
Rollout rolloutRunning = rolloutManagement.create(rolloutRunningCreate, 1, conditions);
Rollout rolloutRunning = rolloutManagement.create(rolloutRunningCreate, 1, false, conditions);
// Let the executor handle created Rollout
rolloutManagement.handleRollouts();
// 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 RolloutCreate rolloutReadyCreate = entityFactory.rollout().create()
.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
rolloutManagement.handleRollouts();
rolloutReady = reloadRollout(rolloutReady);
@@ -1959,7 +2111,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
final RolloutCreate rolloutToCreate = entityFactory.rollout().create().name(rolloutName)
.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
rolloutManagement.handleRollouts();
@@ -1982,8 +2134,14 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
private RolloutGroupCreate generateRolloutGroup(final int index, final Integer percentage,
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")
.targetPercentage(Float.valueOf(percentage)).targetFilterQuery(targetFilter);
.targetPercentage(Float.valueOf(percentage)).targetFilterQuery(targetFilter)
.confirmationRequired(confirmationRequired);
}
private RolloutCreate generateTargetsAndRollout(final String rolloutName, final int amountTargetsForRollout) {
@@ -2033,7 +2191,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
testdataFactory.createTargets(amountTargetsForRollout, targetPrefixName + "-", targetPrefixName);
return testdataFactory.createRolloutByVariables(rolloutName, rolloutName + "description", groupSize,
"controllerId==" + targetPrefixName + "-*", dsForRolloutTwo, successCondition, errorCondition,
Action.ActionType.FORCED, weight);
Action.ActionType.FORCED, weight, false);
}
private int changeStatusForAllRunningActions(final Rollout rollout, final Status status) {

View File

@@ -33,6 +33,9 @@ import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
import org.eclipse.hawkbit.repository.model.TargetType;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
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.data.domain.Pageable;
import org.springframework.data.domain.Slice;
@@ -174,6 +177,69 @@ class AutoAssignCheckerIntTest extends AbstractJpaIntegrationTest {
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
@Description("Test auto assignment of an incomplete DS to filtered targets, that causes failures")
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
private void verifyThatTargetsNotHaveDistributionSetAssignment(final DistributionSet set,
final List<Target> targets) {

View File

@@ -99,7 +99,7 @@ public class RSQLRolloutGroupFieldTest extends AbstractJpaIntegrationTest {
return rolloutManagement.create(
entityFactory.rollout().create().set(distributionSetManagement.get(distributionSetId).get()).name(name)
.targetFilterQuery(targetFilterQuery),
amountGroups, new RolloutGroupConditionBuilder().withDefaults()
amountGroups, false, new RolloutGroupConditionBuilder().withDefaults()
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, "100").build());
}
}

View File

@@ -28,6 +28,7 @@ import org.apache.commons.lang3.RandomStringUtils;
import org.eclipse.hawkbit.artifact.repository.ArtifactRepository;
import org.eclipse.hawkbit.cache.TenantAwareCacheManager;
import org.eclipse.hawkbit.repository.ArtifactManagement;
import org.eclipse.hawkbit.repository.ConfirmationManagement;
import org.eclipse.hawkbit.repository.ControllerManagement;
import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.DistributionSetInvalidationManagement;
@@ -158,6 +159,8 @@ public abstract class AbstractIntegrationTest {
@Autowired
protected DeploymentManagement deploymentManagement;
@Autowired
protected ConfirmationManagement confirmationManagement;
@Autowired
protected DistributionSetInvalidationManagement distributionSetInvalidationManagement;
@@ -235,9 +238,12 @@ public abstract class AbstractIntegrationTest {
protected DistributionSetAssignmentResult assignDistributionSet(final long dsID, final List<String> controllerIds,
final ActionType actionType, final long forcedTime, final Integer weight) {
final boolean confirmationFlowActive = isConfirmationFlowActive();
final List<DeploymentRequest> deploymentRequests = controllerIds.stream()
.map(id -> DeploymentManagement.deploymentRequest(id, dsID).setActionType(actionType)
.setForceTime(forcedTime).setWeight(weight).build())
.setForceTime(forcedTime).setWeight(weight).setConfirmationRequired(confirmationFlowActive)
.build())
.collect(Collectors.toList());
final List<DistributionSetAssignmentResult> results = deploymentManagement
.assignDistributionSets(deploymentRequests);
@@ -245,6 +251,13 @@ public abstract class AbstractIntegrationTest {
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,
final List<Target> targets) {
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)
.setMaintenance(maintenanceWindowSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone)
.build());
.setConfirmationRequired(true).build());
}
protected DistributionSetAssignmentResult assignDistributionSet(final DistributionSet pset, final Target target) {
@@ -312,6 +325,19 @@ public abstract class AbstractIntegrationTest {
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) {
return createDistributionSetMetadata(dsId, Collections.singletonList(md)).get(0);
}
@@ -345,6 +371,10 @@ public abstract class AbstractIntegrationTest {
Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
.getContent().get(0);
if (savedAction.getStatus() == Action.Status.WAIT_FOR_CONFIRMATION) {
confirmationManagement.confirmAction(savedAction.getId(), null, null);
}
savedAction = controllerManagement.addUpdateActionStatus(
entityFactory.actionStatus().create(savedAction.getId()).status(Action.Status.RUNNING));
@@ -473,4 +503,10 @@ public abstract class AbstractIntegrationTest {
protected void disableBatchAssignments() {
tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.BATCH_ASSIGNMENTS_ENABLED, false);
}
protected boolean isConfirmationFlowEnabled() {
return tenantConfigurationManagement
.getConfigurationValue(TenantConfigurationKey.USER_CONFIRMATION_ENABLED, Boolean.class).getValue();
}
}

View File

@@ -877,10 +877,14 @@ public class TestdataFactory {
* @return {@link List} of {@link Target} entities
*/
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);
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);
@@ -1140,7 +1144,14 @@ public class TestdataFactory {
final int groupSize, final String filterQuery, final DistributionSet distributionSet,
final String successCondition, final String errorCondition) {
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
* @param weight
* weight of the Rollout
* @param confirmationRequired
* if the confirmation is required (considered with confirmation flow
* active)
* @return created {@link Rollout}
*/
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 Action.ActionType actionType,
final Integer weight) {
final Integer weight, final boolean confirmationRequired) {
final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder().withDefaults()
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, successCondition)
.errorCondition(RolloutGroupErrorCondition.THRESHOLD, errorCondition)
@@ -1178,7 +1192,7 @@ public class TestdataFactory {
final Rollout rollout = rolloutManagement.create(
entityFactory.rollout().create().name(rolloutName).description(rolloutDescription)
.targetFilterQuery(filterQuery).set(distributionSet).actionType(actionType).weight(weight),
groupSize, conditions);
groupSize, confirmationRequired, conditions);
// Run here, because Scheduler is disabled during tests
rolloutManagement.handleRollouts();
@@ -1314,7 +1328,7 @@ public class TestdataFactory {
createTargets(amountOtherTargets, "others-" + suffix + "-", "rollout");
final String filterQuery = "controllerId==rollout-" + suffix + "-*";
return createRolloutByVariables("rollout-" + suffix, "test-rollout-description", amountOfGroups,
filterQuery, rolloutDS, successCondition, errorCondition, actionType, weight);
filterQuery, rolloutDS, successCondition, errorCondition, actionType, weight, false);
}
/**

View File

@@ -19,8 +19,9 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
/**
* Provide action history information to the controller as part of response to
* {@link DdiRootControllerRestApi#getControllerBasedeploymentAction}: 1.
* Current action status at the server; 2. List of messages from action history
* {@link DdiRootControllerRestApi#getControllerBasedeploymentAction} and
* {@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
* {@link DdiActionFeedback}.
*/

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 +"]";
}
}

View File

@@ -23,6 +23,21 @@ public final class DdiRestConstants {
*/
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.
*/
@@ -52,6 +67,7 @@ public final class DdiRestConstants {
* Default value specifying that no action history to be sent as part of
* response to deploymentBase
* {@link DdiRootControllerRestApi#getControllerBasedeploymentAction}.
* {@link DdiRootControllerRestApi#getConfirmationBaseAction}.
*/
public static final String NO_ACTION_HISTORY = "0";

View File

@@ -16,9 +16,14 @@ import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
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.DdiAutoConfirmationState;
import org.eclipse.hawkbit.ddi.json.model.DdiCancel;
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.DdiDeploymentBase;
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.PostMapping;
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.RequestParam;
@@ -123,17 +129,16 @@ public interface DdiRootControllerRestApi {
* @param controllerId
* of the target
* @param actionId
* of the {@link DdiDeploymentBase} that matches to active
* actions.
* of the {@link DdiDeploymentBase} that matches to active actions.
* @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
* 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
* 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
@@ -212,8 +217,8 @@ public interface DdiRootControllerRestApi {
@PathVariable("actionId") @NotEmpty final Long actionId);
/**
* RequestMethod.POST method receiving the {@link DdiActionFeedback} from
* the target.
* RequestMethod.POST method receiving the {@link DdiActionFeedback} from the
* target.
*
* @param feedback
* the {@link DdiActionFeedback} from the target.
@@ -270,4 +275,119 @@ public interface DdiRootControllerRestApi {
@PathVariable("actionId") @NotEmpty final Long actionId,
@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);
}

View File

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

View File

@@ -31,20 +31,23 @@ import io.qameta.allure.Story;
public class JsonIgnorePropertiesAnnotationTest {
@Test
@Description(
"This test verifies that all model classes within the 'org.eclipse.hawkbit.ddi.json.model' package are annotated with '@JsonIgnoreProperties(ignoreUnknown = true)'")
@Description("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 {
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
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) {
final Class<?> modelClass = classInfo.load();
if (modelClass.getSimpleName().contains("Test") || modelClass.isEnum()) {
continue;
}
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();
}
}

View File

@@ -18,8 +18,10 @@ import org.eclipse.hawkbit.api.URLPlaceholder;
import org.eclipse.hawkbit.api.URLPlaceholder.SoftwareData;
import org.eclipse.hawkbit.ddi.json.model.DdiArtifact;
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.DdiConfig;
import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationBase;
import org.eclipse.hawkbit.ddi.json.model.DdiControllerBase;
import org.eclipse.hawkbit.ddi.json.model.DdiMetadata;
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,
final Action activeAction, final String defaultControllerPollTime, final TenantAware tenantAware) {
final DdiControllerBase result = new DdiControllerBase(
new DdiConfig(new DdiPolling(defaultControllerPollTime)));
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
.linkTo(WebMvcLinkBuilder.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
.getControllerCancelAction(tenantAware.getCurrentTenant(), target.getControllerId(),
@@ -126,10 +161,10 @@ public final class DataConversionHelper {
// have changed from 'soft' to 'forced' type and we need to
// change the payload of the
// response because of eTags.
result.add(WebMvcLinkBuilder
.linkTo(WebMvcLinkBuilder.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
.getControllerBasedeploymentAction(tenantAware.getCurrentTenant(),
target.getControllerId(), activeAction.getId(), calculateEtag(activeAction), null))
result.add(WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder
.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
.getControllerBasedeploymentAction(tenantAware.getCurrentTenant(), target.getControllerId(),
activeAction.getId(), calculateEtag(activeAction), null))
.withRel(DdiRestConstants.DEPLOYMENT_BASE_ACTION));
}
}
@@ -149,6 +184,7 @@ public final class DataConversionHelper {
.putConfigData(null, tenantAware.getCurrentTenant(), target.getControllerId()))
.withRel(DdiRestConstants.CONFIG_DATA_ACTION));
}
return result;
}

View File

@@ -13,6 +13,7 @@ import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
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.ddi.json.model.DdiActionFeedback;
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.DdiAutoConfirmationState;
import org.eclipse.hawkbit.ddi.json.model.DdiCancel;
import org.eclipse.hawkbit.ddi.json.model.DdiCancelActionToStop;
import org.eclipse.hawkbit.ddi.json.model.DdiChunk;
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.DdiDeployment;
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.DdiRootControllerRestApi;
import org.eclipse.hawkbit.repository.ArtifactManagement;
import org.eclipse.hawkbit.repository.ConfirmationManagement;
import org.eclipse.hawkbit.repository.ControllerManagement;
import org.eclipse.hawkbit.repository.EntityFactory;
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.CancelActionNotAllowedException;
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.model.Action;
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 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
private ApplicationEventPublisher eventPublisher;
@@ -155,12 +167,11 @@ public class DdiRootController implements DdiRootControllerRestApi {
checkAndCancelExpiredAction(activeAction);
return new ResponseEntity<>(
DataConversionHelper.fromTarget(target, installedAction, activeAction,
activeAction == null ? controllerManagement.getPollingTime()
: controllerManagement.getPollingTimeForAction(activeAction.getId()),
tenantAware),
HttpStatus.OK);
// activeAction
return new ResponseEntity<>(DataConversionHelper.fromTarget(target, installedAction, activeAction,
activeAction == null ? controllerManagement.getPollingTime()
: controllerManagement.getPollingTimeForAction(activeAction.getId()),
tenantAware), HttpStatus.OK);
}
@Override
@@ -276,7 +287,7 @@ public class DdiRootController implements DdiRootControllerRestApi {
checkAndCancelExpiredAction(action);
if (!action.isCancelingOrCanceled()) {
if (!action.isCancelingOrCanceled() && !action.isWaitingConfirmation()) {
final DdiDeploymentBase base = generateDdiDeploymentBase(target, action, actionHistoryMessageCount);
@@ -324,6 +335,10 @@ public class DdiRootController implements DdiRootControllerRestApi {
final Target target = findTarget(controllerId);
final Action action = findActionForTarget(actionId, target);
if (action.isWaitingConfirmation()) {
return ResponseEntity.notFound().build();
}
if (!action.isActive()) {
LOG.warn("Updating action {} with feedback {} not possible since action not active anymore.",
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,
final Long actionId, final EntityFactory entityFactory) {
@@ -621,4 +614,150 @@ public class DdiRootController implements DdiRootControllerRestApi {
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);
}
}

View File

@@ -24,6 +24,7 @@ import java.util.List;
import org.apache.commons.lang3.RandomStringUtils;
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.DdiResult;
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 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;
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 ResultActions resultActions = performGet(DEPLOYMENT_BASE, mediaType, status().isOk(),
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);
}
@@ -171,60 +179,60 @@ public abstract class AbstractDDiApiIntegrationTest extends AbstractRestIntegrat
final Long osModuleId, final Action.ActionType actionType) throws Exception {
final ResultActions resultActions = performGet(INSTALLED_BASE, mediaType, status().isOk(),
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));
}
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 Long osModuleId, final String downloadType, final String updateType) throws Exception {
return resultActions.andExpect(jsonPath("$.id", equalTo(String.valueOf(actionId))))
.andExpect(jsonPath("$.deployment.download", equalTo(downloadType)))
.andExpect(jsonPath("$.deployment.update", equalTo(updateType)))
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='jvm')].name",
.andExpect(jsonPath(prefix + ".download", equalTo(downloadType)))
.andExpect(jsonPath(prefix + ".update", equalTo(updateType)))
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='jvm')].name",
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())))
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].name",
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].name",
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())))
.andExpect(jsonPath("$.deployment.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].size", contains(ARTIFACT_SIZE)))
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0].filename",
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())))
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[0].hashes.sha1",
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0].hashes.sha1",
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())))
.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
+ "/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
+ "/softwaremodules/" + osModuleId + "/artifacts/" + artifact.getFilename()
+ ".MD5SUM")))
.andExpect(jsonPath("$.deployment.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].size", contains(ARTIFACT_SIZE)))
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1].filename",
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())))
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].artifacts[1].hashes.sha1",
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1].hashes.sha1",
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())))
.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
+ "/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
+ "/softwaremodules/" + osModuleId + "/artifacts/" + artifactSignature.getFilename()
+ ".MD5SUM")))
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='bApp')].version",
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='bApp')].version",
contains(ds.findFirstModuleByType(appType).get().getVersion())))
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='bApp')].metadata").doesNotExist())
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='bApp')].name")
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='bApp')].metadata").doesNotExist())
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='bApp')].name")
.value(ds.findFirstModuleByType(appType).get().getName()));
}
@@ -338,4 +346,24 @@ public abstract class AbstractDDiApiIntegrationTest extends AbstractRestIntegrat
messages);
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);
}
}

View File

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

View File

@@ -26,6 +26,7 @@ public class MgmtTargetAssignmentRequestBody {
private MgmtActionType type;
private MgmtMaintenanceWindowRequestBody maintenanceWindow;
private Integer weight;
private Boolean confirmationRequired;
/**
* JsonCreator Constructor
@@ -77,4 +78,12 @@ public class MgmtTargetAssignmentRequestBody {
public void setMaintenanceWindow(final MgmtMaintenanceWindowRequestBody maintenanceWindow) {
this.maintenanceWindow = maintenanceWindow;
}
public Boolean isConfirmationRequired() {
return confirmationRequired;
}
public void setConfirmationRequired(final boolean confirmationRequired) {
this.confirmationRequired = confirmationRequired;
}
}

View File

@@ -9,6 +9,7 @@
package org.eclipse.hawkbit.mgmt.json.model.rollout;
import java.util.List;
import java.util.Optional;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType;
import org.eclipse.hawkbit.mgmt.json.model.rolloutgroup.MgmtRolloutGroup;
@@ -38,6 +39,9 @@ public class MgmtRolloutRestRequestBody extends AbstractMgmtRolloutConditionsEnt
@JsonProperty(required = false)
private Integer weight;
@JsonProperty(required = false)
private Boolean confirmationRequired;
private MgmtActionType type;
private List<MgmtRolloutGroup> groups;
@@ -161,4 +165,21 @@ public class MgmtRolloutRestRequestBody extends AbstractMgmtRolloutConditionsEnt
public void setWeight(final Integer 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;
}
}

View File

@@ -22,6 +22,7 @@ public class MgmtRolloutGroup extends AbstractMgmtRolloutConditionsEntity {
private String targetFilterQuery;
private Float targetPercentage;
private Boolean confirmationRequired;
public String getTargetFilterQuery() {
return targetFilterQuery;
@@ -38,4 +39,12 @@ public class MgmtRolloutGroup extends AbstractMgmtRolloutConditionsEntity {
public void setTargetPercentage(Float targetPercentage) {
this.targetPercentage = targetPercentage;
}
public Boolean isConfirmationRequired() {
return confirmationRequired;
}
public void setConfirmationRequired(final Boolean confirmationRequired) {
this.confirmationRequired = confirmationRequired;
}
}

View File

@@ -19,6 +19,8 @@ public class MgmtDistributionSetAssignment extends MgmtId {
private long forcetime;
@JsonProperty(required = false)
private Integer weight;
@JsonProperty(required = false)
private Boolean confirmationRequired;
private MgmtActionType type;
private MgmtMaintenanceWindowRequestBody maintenanceWindow;
@@ -77,4 +79,12 @@ public class MgmtDistributionSetAssignment extends MgmtId {
public void setMaintenanceWindow(final MgmtMaintenanceWindowRequestBody maintenanceWindow) {
this.maintenanceWindow = maintenanceWindow;
}
public Boolean isConfirmationRequired() {
return confirmationRequired;
}
public void setConfirmationRequired(final Boolean confirmationRequired) {
this.confirmationRequired = confirmationRequired;
}
}

View File

@@ -55,6 +55,9 @@ public class MgmtTarget extends MgmtNamedEntity {
@JsonProperty
private String targetTypeName;
@JsonProperty
private Boolean autoConfirmActive;
/**
* @return Target type ID
*/
@@ -221,4 +224,13 @@ public class MgmtTarget extends MgmtNamedEntity {
public void setRequestAttributes(final boolean 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