Mgmt/actions confirm (#2271)

* Extend MGMT API to be possible to confirm/deny Actions on Targets as Operators.

* Added tests

* Fixed permissions in api doc

* added missing license header

---------

Co-authored-by: vasilchev <vasil.ilchev@bosch.com>
This commit is contained in:
Vasil Ilchev
2025-02-13 09:10:24 +02:00
committed by GitHub
parent 91bf70626c
commit 64ffc6a27a
4 changed files with 285 additions and 3 deletions

View File

@@ -15,6 +15,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -27,6 +28,7 @@ import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadata;
import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadataBodyPut;
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
import org.eclipse.hawkbit.mgmt.json.model.action.MgmtAction;
import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionConfirmationRequestBodyPut;
import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionRequestBodyPut;
import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionStatus;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType;
@@ -48,6 +50,7 @@ import org.eclipse.hawkbit.repository.OffsetBasedPageRequest;
import org.eclipse.hawkbit.repository.TargetManagement;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.InvalidConfirmationFeedbackException;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.ActionStatus;
import org.eclipse.hawkbit.repository.model.DeploymentRequest;
@@ -226,14 +229,19 @@ public class MgmtTargetResource implements MgmtTargetRestApi {
@Override
public ResponseEntity<MgmtAction> getAction(final String targetId, final Long actionId) {
return getValidatedAction(targetId, actionId)
.map(action -> ResponseEntity.ok(MgmtTargetMapper.toResponseWithLinks(targetId, action)))
.orElseGet(() -> ResponseEntity.notFound().build());
}
private Optional<Action> getValidatedAction(final String targetId, final Long actionId) {
final Action action = deploymentManagement.findAction(actionId)
.orElseThrow(() -> new EntityNotFoundException(Action.class, actionId));
if (!action.getTarget().getControllerId().equals(targetId)) {
log.warn(ACTION_TARGET_MISSING_ASSIGN_WARN, action.getId(), targetId);
return ResponseEntity.notFound().build();
return Optional.empty();
}
return ResponseEntity.ok(MgmtTargetMapper.toResponseWithLinks(targetId, action));
return Optional.of(action);
}
@Override
@@ -276,6 +284,46 @@ public class MgmtTargetResource implements MgmtTargetRestApi {
return ResponseEntity.ok(MgmtTargetMapper.toResponseWithLinks(targetId, action));
}
@Override
public ResponseEntity<Void> updateActionConfirmation(final String targetId, final Long actionId,
final MgmtActionConfirmationRequestBodyPut actionConfirmation) {
log.debug("updateActionConfirmation with data [targetId={}, actionId={}]: {}", targetId, actionId, actionConfirmation);
return getValidatedAction(targetId, actionId).map(action -> {
try {
switch (actionConfirmation.getConfirmation()) {
case CONFIRMED:
log.info("Confirmed the action (actionId: {}, targetId: {}) as we got {} report",
actionId, targetId, actionConfirmation.getConfirmation());
confirmationManagement.confirmAction(actionId, actionConfirmation.getCode(), actionConfirmation.getDetails());
break;
case DENIED:
default:
log.debug("Controller denied the action (actionId: {}, controllerId: {}) as we got {} report.",
actionId, targetId, actionConfirmation.getConfirmation());
confirmationManagement.denyAction(actionId, actionConfirmation.getCode(), actionConfirmation.getDetails());
break;
}
return new ResponseEntity<Void>(HttpStatus.OK);
} 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(), actionConfirmation.getConfirmation(), e);
return new ResponseEntity<Void>(HttpStatus.GONE);
} else if (e.getReason() == InvalidConfirmationFeedbackException.Reason.NOT_AWAITING_CONFIRMATION) {
log.debug("Action is not waiting for confirmation, deny request.", e);
return new ResponseEntity<Void>(HttpStatus.NOT_FOUND);
} else {
log.debug("Action confirmation failed with unknown reason.", e);
return new ResponseEntity<Void>(HttpStatus.BAD_REQUEST);
}
}
}).orElseGet(() -> {
log.warn("Action {} not found for target {}", actionId, targetId);
return ResponseEntity.notFound().build();
});
}
@Override
public ResponseEntity<PagedList<MgmtActionStatus>> getActionStatusList(
final String targetId, final Long actionId,

View File

@@ -55,6 +55,7 @@ import io.qameta.allure.Story;
import org.awaitility.Awaitility;
import org.eclipse.hawkbit.exception.SpServerError;
import org.eclipse.hawkbit.im.authentication.SpPermission;
import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionConfirmationRequestBodyPut;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetAutoConfirmUpdate;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants;
@@ -63,6 +64,8 @@ import org.eclipse.hawkbit.repository.ActionFields;
import org.eclipse.hawkbit.repository.Identifiable;
import org.eclipse.hawkbit.repository.builder.ActionStatusCreate;
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
import org.eclipse.hawkbit.repository.jpa.repository.ActionRepository;
import org.eclipse.hawkbit.repository.jpa.specifications.ActionSpecifications;
@@ -280,6 +283,101 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest {
.andExpect(status().isOk());
}
@Test
@Description("Test confirmation of single Action with confirm status. Check that Action goes into Running status with appropriate messages and status code")
public void updateActionConfirmationWithConfirm() throws Exception {
final int expectedStatusCode = 210;
final String expectedStatusMessage1 = "some-custom-message1";
final String expectedStatusMessage2 = "some-custom-message2";
final Status expectedStatusAfterActionConfirmationCall = Status.RUNNING;
final long actionId = doAssignmentAndTestConfirmation("targetId");
testActionConfirmation("targetId", actionId, MgmtActionConfirmationRequestBodyPut.Confirmation.CONFIRMED, expectedStatusCode, new String[]{expectedStatusMessage1, expectedStatusMessage2}, HttpStatus.OK ,expectedStatusAfterActionConfirmationCall);
}
@Test
@Description("Test confirmation of single Action with deny status. Check that Action stays in WAIT_FOR_CONFIRMATION status with appropriate messages and status code")
public void updateActionConfirmationWithDeny() throws Exception {
final int expectedStatusCode = 410;
final String expectedStatusMessage1 = "some-error-custom-message1";
final String expectedStatusMessage2 = "some-error-custom-message2";
final Status expectedStatusAfterActionConfirmationCall = Status.WAIT_FOR_CONFIRMATION;
final long actionId = doAssignmentAndTestConfirmation("targetId");
testActionConfirmation("targetId", actionId, MgmtActionConfirmationRequestBodyPut.Confirmation.DENIED, expectedStatusCode, new String[]{expectedStatusMessage1, expectedStatusMessage2}, HttpStatus.OK, expectedStatusAfterActionConfirmationCall);
}
@Test
@Description("Test confirmation of single Action with wrong ControllerId - e.g. the given Action is not assigned to the given Target - confirmation call must fail.")
public void updateActionConfirmationFailsIfActionNotAssignedToTarget() throws Exception {
final int payloadCallCode = 200;
final String payloadCallMessage1 = "random1";
final String payloadCallMessage2 = "random2";
final long controller1Action = doAssignmentAndTestConfirmation("controller1");
final long controller2Action = doAssignmentAndTestConfirmation("controller2");
// test that target id and action id are checked correctly and only actions assigned to given targets are confirmed/denied
// if action is not assigned to the target, confirmation call must fail
testActionConfirmation("controller1", controller2Action, MgmtActionConfirmationRequestBodyPut.Confirmation.CONFIRMED,
payloadCallCode, new String[]{payloadCallMessage1, payloadCallMessage2}, HttpStatus.NOT_FOUND,
Status.WAIT_FOR_CONFIRMATION);
testActionConfirmation("controller2", controller1Action, MgmtActionConfirmationRequestBodyPut.Confirmation.CONFIRMED,
payloadCallCode, new String[]{payloadCallMessage1, payloadCallMessage2}, HttpStatus.NOT_FOUND,
Status.WAIT_FOR_CONFIRMATION);
}
long doAssignmentAndTestConfirmation(final String controllerId) {
enableConfirmationFlow();
final Target testTarget = testdataFactory.createTarget(controllerId);
final DistributionSet dsA = testdataFactory.createDistributionSet(controllerId);
assignDistributionSet(dsA, Collections.singletonList(testTarget));
// check initial status after assignment is done with Confirmation Flow enabled
// expected Actions to be in WAIT_FOR_CONFIRMATION status
List<Action> actionHistory = deploymentManagement.findActionsByTarget(controllerId, PAGE).getContent();
assertThat(actionHistory).hasSize(1);
Action action = actionHistory.get(0);
assertThat(action.getStatus()).isEqualTo(Status.WAIT_FOR_CONFIRMATION);
return action.getId();
}
void testActionConfirmation(final String controllerId, final long actionId, final MgmtActionConfirmationRequestBodyPut.Confirmation payloadConfirmation, final int payloadCode, final String[] payloadMessages, final HttpStatus expectedHttpResponseStatus,final Status expectedGeneratedStatus) throws Exception {
String url = MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + controllerId + "/" + MgmtRestConstants.TARGET_V1_ACTIONS + "/" + actionId + "/confirmation";
mvc.perform(put(url)
.content(String.format("{\"confirmation\":\"%s\",\"details\":[\"%s\",\"%s\"],\"code\":%d}",
payloadConfirmation.getName(),
payloadMessages[0],
payloadMessages[1],
payloadCode
))
.contentType(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().is(expectedHttpResponseStatus.value()));
// check status after confirmation is done (either confirmed or denied)
final List<Action> actionHistory = deploymentManagement.findActionsByTarget(controllerId, PAGE).getContent();
assertThat(actionHistory).hasSize(1);
final JpaAction jpaAction = (JpaAction) actionHistory.get(0);
final List<ActionStatus> actionStatuses = new ArrayList<>(jpaAction.getActionStatus());
// confirmation call was successful, check if Action status ,status code and messages are updated appropriately
if (expectedHttpResponseStatus == HttpStatus.OK) {
assertThat(jpaAction.getStatus()).isEqualTo(expectedGeneratedStatus);
assertThat(jpaAction.getLastActionStatusCode().get()).isEqualTo(payloadCode);
actionStatuses.sort(Comparator.comparingLong(Identifiable::getId));
assertThat(actionStatuses.size()).isEqualTo(2);
assertThat((actionStatuses.get(0)).getStatus()).isEqualTo(Status.WAIT_FOR_CONFIRMATION);
assertThat((actionStatuses.get(0)).getCode().isEmpty()).isTrue();
assertThat((actionStatuses.get(1)).getStatus()).isEqualTo(expectedGeneratedStatus);
assertThat((actionStatuses.get(1)).getCode().get()).isEqualTo(payloadCode);
assertThat(((JpaActionStatus) actionStatuses.get(1)).getMessages()).contains(payloadMessages[0], payloadMessages[1]);
} else { // confirmation call not successful, check if Action status is not updated, no new Action status added as well.
assertThat(jpaAction.getStatus()).isEqualTo(Status.WAIT_FOR_CONFIRMATION);
assertThat(jpaAction.getLastActionStatusCode().isEmpty()).isTrue();
assertThat(jpaAction.getActionStatus()).hasSize(1);
}
}
@Test
@Description("Ensures that actions list is in expected order.")
void getActionStatusReturnsCorrectType() throws Exception {