Introduce target grouping (#2538)
* Introduce target grouping Signed-off-by: strailov <Stanislav.Trailov@bosch.io> * minor refactor Signed-off-by: strailov <Stanislav.Trailov@bosch.io> * throw validation exception instead direct returning bad request response Signed-off-by: strailov <Stanislav.Trailov@bosch.io> * fix group query parameter Signed-off-by: strailov <Stanislav.Trailov@bosch.io> * remove wrongly added import Signed-off-by: strailov <Stanislav.Trailov@bosch.io> * add review fixes Signed-off-by: strailov <Stanislav.Trailov@bosch.io> * apply latest review changes Signed-off-by: strailov <Stanislav.Trailov@bosch.io> * apply latest changes after sybnc/review Signed-off-by: strailov <Stanislav.Trailov@bosch.io> * fix after review Signed-off-by: strailov <Stanislav.Trailov@bosch.io> --------- Signed-off-by: strailov <Stanislav.Trailov@bosch.io>
This commit is contained in:
committed by
GitHub
parent
e7373275bf
commit
b4793fcce1
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.mgmt.rest.resource;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTarget;
|
||||
import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetGroupRestApi;
|
||||
import org.eclipse.hawkbit.mgmt.rest.resource.mapper.MgmtTargetMapper;
|
||||
import org.eclipse.hawkbit.mgmt.rest.resource.util.PagingUtility;
|
||||
import org.eclipse.hawkbit.repository.TargetManagement;
|
||||
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.security.SystemSecurityContext;
|
||||
import org.eclipse.hawkbit.utils.TenantConfigHelper;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.eclipse.hawkbit.mgmt.rest.resource.util.PagingUtility.sanitizeTargetSortParam;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
public class MgmtTargetGroupResource implements MgmtTargetGroupRestApi {
|
||||
|
||||
private final TargetManagement targetManagement;
|
||||
private final TenantConfigHelper tenantConfigHelper;
|
||||
|
||||
public MgmtTargetGroupResource(final TargetManagement targetManagement, final SystemSecurityContext systemSecurityContext,
|
||||
final TenantConfigurationManagement tenantConfigurationManagement) {
|
||||
this.targetManagement = targetManagement;
|
||||
this.tenantConfigHelper = TenantConfigHelper.usingContext(systemSecurityContext, tenantConfigurationManagement);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<PagedList<MgmtTarget>> getAssignedTargets(String group, int pagingOffsetParam, int pagingLimitParam, String sortParam) {
|
||||
final Pageable pageable = PagingUtility.toPageable(pagingOffsetParam, pagingLimitParam, sanitizeTargetSortParam(sortParam));
|
||||
|
||||
final Page<Target> targets = targetManagement.findTargetsByGroup(group, false, pageable);
|
||||
|
||||
final List<MgmtTarget> rest = MgmtTargetMapper.toResponse(targets.getContent(), tenantConfigHelper);
|
||||
return ResponseEntity.ok(new PagedList<>(rest, targets.getTotalElements()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<PagedList<MgmtTarget>> getAssignedTargetsWithSubgroups(String groupFilter, boolean subgroups, int pagingOffsetParam, int pagingLimitParam, String sortParam) {
|
||||
final Pageable pageable = PagingUtility.toPageable(pagingOffsetParam, pagingLimitParam, sanitizeTargetSortParam(sortParam));
|
||||
|
||||
final Page<Target> targets = targetManagement.findTargetsByGroup(groupFilter, subgroups, pageable);
|
||||
|
||||
final List<MgmtTarget> rest = MgmtTargetMapper.toResponse(targets.getContent(), tenantConfigHelper);
|
||||
return ResponseEntity.ok(new PagedList<>(rest, targets.getTotalElements()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> assignTargetsToGroup(String group, List<String> controllerIds) {
|
||||
return assignTargets(group, controllerIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> assignTargetsToGroupWithSubgroups(String group, List<String> controllerIds) {
|
||||
return assignTargets(group, controllerIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> assignTargetsToGroupWithRsql(String group, String rsql) {
|
||||
targetManagement.assignTargetGroupWithRsql(group, rsql);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> unassignTargetsFromGroup(List<String> controllerIds) {
|
||||
targetManagement.assignTargetsWithGroup(null, controllerIds);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> unassignTargetsFromGroupByRsql(String rsql) {
|
||||
targetManagement.assignTargetGroupWithRsql(null, rsql);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<List<String>> getTargetGroups() {
|
||||
final List<String> groups = targetManagement.findGroups();
|
||||
return ResponseEntity.ok(groups);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> assignTargetsToGroup(final String group, final String rsql) {
|
||||
targetManagement.assignTargetGroupWithRsql(group, rsql);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
private ResponseEntity<Void> assignTargets(final String group, final List<String> controllerIds) {
|
||||
targetManagement.assignTargetsWithGroup(group, controllerIds);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
@@ -140,6 +140,7 @@ public final class MgmtTargetMapper {
|
||||
targetRest.setDescription(target.getDescription());
|
||||
targetRest.setName(target.getName());
|
||||
targetRest.setUpdateStatus(target.getUpdateStatus().name().toLowerCase());
|
||||
targetRest.setGroup(target.getGroup());
|
||||
|
||||
final URI address = target.getAddress();
|
||||
if (address != null) {
|
||||
@@ -325,7 +326,7 @@ public final class MgmtTargetMapper {
|
||||
private static TargetCreate fromRequest(final EntityFactory entityFactory, final MgmtTargetRequestBody targetRest) {
|
||||
return entityFactory.target().create().controllerId(targetRest.getControllerId()).name(targetRest.getName())
|
||||
.description(targetRest.getDescription()).securityToken(targetRest.getSecurityToken())
|
||||
.address(targetRest.getAddress()).targetType(targetRest.getTargetType());
|
||||
.address(targetRest.getAddress()).targetType(targetRest.getTargetType()).group(targetRest.getGroup());
|
||||
}
|
||||
|
||||
private static String getType(final Action action) {
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.mgmt.rest.resource;
|
||||
|
||||
import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants;
|
||||
import org.eclipse.hawkbit.rest.util.JsonBuilder;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
|
||||
public class MgmtTargetGroupResourceTest extends AbstractManagementApiIntegrationTest {
|
||||
|
||||
|
||||
@Test
|
||||
void shouldRetrieveDistinctTargetGroups() throws Exception {
|
||||
|
||||
final List<String> expectedGroups = List.of("Europe", "Asia");
|
||||
targetManagement.create(entityFactory.target().create().controllerId("target1").group("Europe"));
|
||||
targetManagement.create(entityFactory.target().create().controllerId("target2").group("Asia"));
|
||||
targetManagement.create(entityFactory.target().create().controllerId("target3").group("Europe"));
|
||||
|
||||
mvc.perform(get(MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING)
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.[0]", Matchers.in(expectedGroups)))
|
||||
.andExpect(jsonPath("$.[1]", Matchers.in(expectedGroups)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRetrieveTargetsFilteredByGroupAndParentGroupCorrectly() throws Exception {
|
||||
targetManagement.create(entityFactory.target().create().controllerId("target1").group("Europe/West"));
|
||||
targetManagement.create(entityFactory.target().create().controllerId("target2").group("Europe/East"));
|
||||
targetManagement.create(entityFactory.target().create().controllerId("target3").group("Europe"));
|
||||
|
||||
mvc.perform(get(MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING + "/assigned")
|
||||
.param("group", "Europe/East")
|
||||
.param("subgroups", "false"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("content", Matchers.hasSize(1)))
|
||||
.andExpect(jsonPath("content.[0].controllerId", Matchers.equalTo("target2")));
|
||||
|
||||
mvc.perform(get(MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING + "/assigned")
|
||||
.param("group", "Europe")
|
||||
.param("subgroups", "true")
|
||||
.param(MgmtRestConstants.REQUEST_PARAMETER_SORTING, "ID:ASC"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("content", Matchers.hasSize(3)))
|
||||
.andExpect(jsonPath("content.[0].controllerId", Matchers.equalTo("target1")))
|
||||
.andExpect(jsonPath("content.[1].controllerId", Matchers.equalTo("target2")))
|
||||
.andExpect(jsonPath("content.[2].controllerId", Matchers.equalTo("target3")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetAssignedTargetsToSpecificGroup() throws Exception {
|
||||
targetManagement.create(entityFactory.target().create().controllerId("target1").group("Europe"));
|
||||
targetManagement.create(entityFactory.target().create().controllerId("target2").group("US"));
|
||||
targetManagement.create(entityFactory.target().create().controllerId("target3").group("Europe"));
|
||||
|
||||
mvc.perform(get(MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING + "/Europe/assigned")
|
||||
.param(MgmtRestConstants.REQUEST_PARAMETER_SORTING, "ID:ASC"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("content", Matchers.hasSize(2)))
|
||||
.andExpect(jsonPath("content.[0].controllerId", Matchers.equalTo("target1")))
|
||||
.andExpect(jsonPath("content.[1].controllerId", Matchers.equalTo("target3")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAssignListOfTargetsToASpecificGroup() throws Exception {
|
||||
targetManagement.create(entityFactory.target().create().controllerId("target1").group("Europe"));
|
||||
targetManagement.create(entityFactory.target().create().controllerId("target2"));
|
||||
targetManagement.create(entityFactory.target().create().controllerId("target3").group("Europe"));
|
||||
|
||||
mvc.perform(put(MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING + "/newGroup/assigned")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(JsonBuilder.toArray(Arrays.asList("target1", "target2", "target3"))))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
|
||||
mvc.perform(get(MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING + "/newGroup/assigned")
|
||||
.param(MgmtRestConstants.REQUEST_PARAMETER_SORTING, "ID:ASC")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("content", Matchers.hasSize(3)))
|
||||
.andExpect(jsonPath("content.[0].controllerId", Matchers.equalTo("target1")))
|
||||
.andExpect(jsonPath("content.[1].controllerId", Matchers.equalTo("target2")))
|
||||
.andExpect(jsonPath("content.[2].controllerId", Matchers.equalTo("target3")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnBadRequestWhenProvidingAnEmptyListOfControllerIds() throws Exception {
|
||||
mvc.perform(put(MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING + "/someGroup/assigned")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(JsonBuilder.toArray(Collections.emptyList())))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAssignListOfTargetsToProvidedGroupWithSubgroup() throws Exception {
|
||||
targetManagement.create(entityFactory.target().create().controllerId("target1").group("Europe"));
|
||||
targetManagement.create(entityFactory.target().create().controllerId("target2"));
|
||||
targetManagement.create(entityFactory.target().create().controllerId("target3").group("US"));
|
||||
|
||||
mvc.perform(put(MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING + "/assigned")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(JsonBuilder.toArray(Arrays.asList("target1", "target2", "target3")))
|
||||
.param("group", "Europe/East"))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
mvc.perform(get(MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING + "/assigned")
|
||||
.param(MgmtRestConstants.REQUEST_PARAMETER_SORTING, "ID:ASC")
|
||||
.param("group", "Europe/East")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("content", Matchers.hasSize(3)))
|
||||
.andExpect(jsonPath("content.[0].controllerId", Matchers.equalTo("target1")))
|
||||
.andExpect(jsonPath("content.[1].controllerId", Matchers.equalTo("target2")))
|
||||
.andExpect(jsonPath("content.[2].controllerId", Matchers.equalTo("target3")));
|
||||
|
||||
// expect bad request if empty controllerIds
|
||||
mvc.perform(put(MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING + "/assigned")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(JsonBuilder.toArray(Collections.emptyList()))
|
||||
.param("group", "doesNotMatter"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAssignTargetsToProvidedGroupByRsql() throws Exception {
|
||||
targetManagement.create(entityFactory.target().create().controllerId("target1").group("A"));
|
||||
targetManagement.create(entityFactory.target().create().controllerId("target2"));
|
||||
targetManagement.create(entityFactory.target().create().controllerId("shouldNotAssign").group("B"));
|
||||
|
||||
mvc.perform(put(MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING + "/C")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.param("q", "controllerId==target*"))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
mvc.perform(get(MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING + "/assigned")
|
||||
.param(MgmtRestConstants.REQUEST_PARAMETER_SORTING, "ID:ASC")
|
||||
.param("group", "C")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("content", Matchers.hasSize(2)))
|
||||
.andExpect(jsonPath("content.[0].controllerId", Matchers.equalTo("target1")))
|
||||
.andExpect(jsonPath("content.[1].controllerId", Matchers.equalTo("target2")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUnassignTargetsFromGroup() throws Exception {
|
||||
targetManagement.create(entityFactory.target().create().controllerId("target1").group("Europe"));
|
||||
targetManagement.create(entityFactory.target().create().controllerId("target2").group("Europe"));
|
||||
|
||||
mvc.perform(delete(MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING + "/assigned")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(JsonBuilder.toArray(Arrays.asList("target1", "target2"))))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
mvc.perform(get(MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING + "/assigned")
|
||||
.param(MgmtRestConstants.REQUEST_PARAMETER_SORTING, "ID:ASC")
|
||||
.param("group", "Europe")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("content", Matchers.hasSize(0)));
|
||||
|
||||
// expect bad request if empty
|
||||
mvc.perform(delete(MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING + "/assigned")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(JsonBuilder.toArray(Collections.emptyList())))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUnassignTargetsFromGroupByRsqlFilter() throws Exception {
|
||||
targetManagement.create(entityFactory.target().create().controllerId("target1").group("Europe"));
|
||||
targetManagement.create(entityFactory.target().create().controllerId("target2").group("Europe"));
|
||||
targetManagement.create(entityFactory.target().create().controllerId("nonMatchingTarget").group("Europe"));
|
||||
|
||||
mvc.perform(delete(MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.param("q", "controllerId==target*"))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
mvc.perform(get(MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING + "/assigned")
|
||||
.param(MgmtRestConstants.REQUEST_PARAMETER_SORTING, "ID:ASC")
|
||||
.param("group", "Europe")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("content", Matchers.hasSize(1)))
|
||||
.andExpect(jsonPath("content.[0].controllerId", Matchers.equalTo("nonMatchingTarget")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUpdateTargetGroupsOfTargetsMatchingTheRsqlFilter() throws Exception {
|
||||
targetManagement.create(entityFactory.target().create().controllerId("target1").group("Europe"));
|
||||
targetManagement.create(entityFactory.target().create().controllerId("target2").group("Europe"));
|
||||
targetManagement.create(entityFactory.target().create().controllerId("target3").group("Europe"));
|
||||
targetManagement.create(entityFactory.target().create().controllerId("shouldNotBeUpdated1").group("Europe"));
|
||||
targetManagement.create(entityFactory.target().create().controllerId("shouldNotBeUpdated2").group("Europe"));
|
||||
|
||||
mvc.perform(put(MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.param("group", "Europe/East")
|
||||
.param("q", "controllerId==target*"))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
mvc.perform(get(MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING + "/assigned")
|
||||
.param("group", "Europe/East")
|
||||
.param(MgmtRestConstants.REQUEST_PARAMETER_SORTING, "ID:ASC")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("content", Matchers.hasSize(3)))
|
||||
.andExpect(jsonPath("content.[0].controllerId", Matchers.equalTo("target1")))
|
||||
.andExpect(jsonPath("content.[1].controllerId", Matchers.equalTo("target2")))
|
||||
.andExpect(jsonPath("content.[2].controllerId", Matchers.equalTo("target3")));
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user