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
@@ -29,6 +29,7 @@ public enum TargetFields implements RsqlQueryField {
|
||||
UPDATESTATUS("updateStatus"),
|
||||
IPADDRESS("address"),
|
||||
ATTRIBUTE("controllerAttributes"),
|
||||
GROUP("group"),
|
||||
ASSIGNEDDS(
|
||||
"assignedDistributionSet",
|
||||
DistributionSetFields.NAME.getJpaEntityFieldName(), DistributionSetFields.VERSION.getJpaEntityFieldName()),
|
||||
|
||||
@@ -90,6 +90,9 @@ public class MgmtTarget extends MgmtNamedEntity {
|
||||
@Schema(description = "Controller ID", example = "123")
|
||||
private String controllerId;
|
||||
|
||||
@Schema(description = "Target group", example = "Europe/East")
|
||||
private String group;
|
||||
|
||||
@Schema(description = "If the target is in sync", example = "in_sync")
|
||||
private String updateStatus;
|
||||
|
||||
|
||||
@@ -42,4 +42,7 @@ public class MgmtTargetRequestBody {
|
||||
|
||||
@Schema(description = "ID of the target type", example = "10")
|
||||
private Long targetType;
|
||||
|
||||
@Schema(description = "Target group", example = "Asia")
|
||||
private String group;
|
||||
}
|
||||
@@ -46,6 +46,10 @@ public final class MgmtRestConstants {
|
||||
* The target type URL mapping rest resource.
|
||||
*/
|
||||
public static final String TARGETTYPE_V1_REQUEST_MAPPING = BASE_V1_REQUEST_MAPPING + "/targettypes";
|
||||
/**
|
||||
* The target group URL mapping rest resource.
|
||||
*/
|
||||
public static final String TARGET_GROUP_V1_REQUEST_MAPPING = BASE_V1_REQUEST_MAPPING + "/targetgroups";
|
||||
/**
|
||||
* The tag URL mapping rest resource.
|
||||
*/
|
||||
@@ -162,6 +166,10 @@ public final class MgmtRestConstants {
|
||||
* The tag URL mapping rest resource.
|
||||
*/
|
||||
public static final String DISTRIBUTIONSET_TAG_DISTRIBUTIONSETS_REQUEST_MAPPING = "/{distributionsetTagId}/assigned";
|
||||
/**
|
||||
* Target group URL mapping rest resource
|
||||
*/
|
||||
public static final String TARGET_GROUP_TARGETS_REQUEST_MAPPING = "/{group}/assigned";
|
||||
/**
|
||||
* The default offset parameter in case the offset parameter is not present in the request.
|
||||
*
|
||||
@@ -233,6 +241,7 @@ public final class MgmtRestConstants {
|
||||
public static final String TARGET_TAG_ORDER = "2000";
|
||||
public static final String TARGET_TYPE_ORDER = "3000";
|
||||
public static final String TARGET_FILTER_ORDER = "4000";
|
||||
public static final String TARGET_GROUP_ORDER = "4500";
|
||||
public static final String ACTION_ORDER = "5000";
|
||||
public static final String ROLLOUT_ORDER = "6000";
|
||||
public static final String DISTRIBUTION_SET_ORDER = "7000";
|
||||
|
||||
@@ -0,0 +1,370 @@
|
||||
/**
|
||||
* 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.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.extensions.Extension;
|
||||
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTarget;
|
||||
import org.eclipse.hawkbit.rest.OpenApi;
|
||||
import org.eclipse.hawkbit.rest.json.model.ExceptionInfo;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.TARGET_GROUP_ORDER;
|
||||
|
||||
@Tag(
|
||||
name = "Target Groups", description = "REST API for Target Groups operations.",
|
||||
extensions = @Extension(name = OpenApi.X_HAWKBIT, properties = @ExtensionProperty(name = "order", value = TARGET_GROUP_ORDER)))
|
||||
public interface MgmtTargetGroupRestApi {
|
||||
|
||||
|
||||
/**
|
||||
* Handles the GET request of retrieving a list of assigned targets for a specific group. Complex grouping (subgroups) not supported here.
|
||||
* For complex grouping use the analogical resource with query parameter for target group.
|
||||
*
|
||||
* @param group - target group
|
||||
* @param pagingOffsetParam the offset of list of targets for pagination, might not be present in the rest request then default value will
|
||||
* be applied
|
||||
* @param pagingLimitParam the limit of the paged request, might not be present in the rest request then default value will be applied
|
||||
* @param sortParam the sorting parameter in the request URL, syntax {@code field:direction, field:direction}
|
||||
* @return a list of targets matching the provided group for a defined or default page request with status OK. The response is always paged. In any failure the
|
||||
* JsonResponseExceptionHandler is handling the response.
|
||||
*/
|
||||
@Operation(summary = "Return assigned targets for group",
|
||||
description = "Handles the GET request of retrieving a list of assigned targets for a specific group. Complex grouping (subgroups) not supported here." +
|
||||
"For complex grouping use the analogical resource with query parameter for target group.")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "Successfully retrieved"),
|
||||
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
|
||||
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))),
|
||||
@ApiResponse(responseCode = "401", description = "The request requires user authentication."),
|
||||
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be " +
|
||||
"changed (i.e. read-only) or data volume restriction applies."),
|
||||
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource."),
|
||||
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json."),
|
||||
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts " +
|
||||
"and the client has to wait another second.")
|
||||
})
|
||||
@GetMapping(value = MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING + MgmtRestConstants.TARGET_GROUP_TARGETS_REQUEST_MAPPING,
|
||||
produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE })
|
||||
ResponseEntity<PagedList<MgmtTarget>> getAssignedTargets(
|
||||
@PathVariable
|
||||
@Schema(description = "The target group of the targets")
|
||||
String group,
|
||||
@RequestParam(
|
||||
value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_OFFSET,
|
||||
defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET)
|
||||
@Schema(description = "The paging offset (default is 0)")
|
||||
int pagingOffsetParam,
|
||||
@RequestParam(
|
||||
value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT,
|
||||
defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT)
|
||||
@Schema(description = "The maximum number of entries in a page (default is 50)")
|
||||
int pagingLimitParam,
|
||||
@RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SORTING, required = false)
|
||||
@Schema(description = """
|
||||
The query parameter sort allows to define the sort order for the result of a query. A sort criteria
|
||||
consists of the name of a field and the sort direction (ASC for ascending and DESC descending).
|
||||
The sequence of the sort criteria (multiple can be used) defines the sort order of the entities
|
||||
in the result.""")
|
||||
String sortParam
|
||||
);
|
||||
|
||||
/**
|
||||
* Handles the GET request of retrieving a list of assigned targets for a specific group. Complex grouping (subgroups) is supported here.
|
||||
* Search could be for specific group, complex group e.g Parent/Child or also for groups including its subgroups
|
||||
*
|
||||
* @param groupFilter - An Actual group - Parent/Child or Parent
|
||||
* @param subgroups - If set to {@code true} enables the search in subgroups
|
||||
* @param pagingOffsetParam the offset of list of targets for pagination, might not be present in the rest request then default value will
|
||||
* be applied
|
||||
* @param pagingLimitParam the limit of the paged request, might not be present in the rest request then default value will be applied
|
||||
* @param sortParam the sorting parameter in the request URL, syntax {@code field:direction, field:direction}
|
||||
* @return a list of targets matching the provided group for a defined or default page request with status OK. The response is always paged. In any failure the
|
||||
* JsonResponseExceptionHandler is handling the response.
|
||||
*/
|
||||
@Operation(summary = "Return assigned targets for group",
|
||||
description = "Handles the GET request of retrieving a list of assigned targets for a specific group. Complex grouping (subgroups) is supported here." +
|
||||
"Search could be for specific group, complex group e.g Parent/Child or also for groups including its subgroups")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "Successfully retrieved"),
|
||||
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
|
||||
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))),
|
||||
@ApiResponse(responseCode = "401", description = "The request requires user authentication."),
|
||||
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be " +
|
||||
"changed (i.e. read-only) or data volume restriction applies."),
|
||||
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource."),
|
||||
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json."),
|
||||
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts " +
|
||||
"and the client has to wait another second.")
|
||||
})
|
||||
@GetMapping(value = MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING + "/assigned")
|
||||
ResponseEntity<PagedList<MgmtTarget>> getAssignedTargetsWithSubgroups(
|
||||
@RequestParam(value = "group")
|
||||
@Schema(description = "Target Group or Filter based on target groups. ")
|
||||
String groupFilter,
|
||||
@RequestParam(value = "subgroups", defaultValue = "false")
|
||||
@Schema(description = " Possibility to search for subgroups with wildcard or not - e.g. ParentGroup/*")
|
||||
boolean subgroups,
|
||||
@RequestParam(
|
||||
value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_OFFSET,
|
||||
defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET)
|
||||
@Schema(description = "The paging offset (default is 0)")
|
||||
int pagingOffsetParam,
|
||||
@RequestParam(
|
||||
value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT,
|
||||
defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT)
|
||||
@Schema(description = "The maximum number of entries in a page (default is 50)")
|
||||
int pagingLimitParam,
|
||||
@RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SORTING, required = false)
|
||||
@Schema(description = """
|
||||
The query parameter sort allows to define the sort order for the result of a query. A sort criteria
|
||||
consists of the name of a field and the sort direction (ASC for ascending and DESC descending).
|
||||
The sequence of the sort criteria (multiple can be used) defines the sort order of the entities
|
||||
in the result.""")
|
||||
String sortParam
|
||||
);
|
||||
|
||||
/**
|
||||
* Assigns targets to a given group.
|
||||
* For complex groups use analogical method with query parameters.
|
||||
*
|
||||
* @param group - target group to be assigned
|
||||
* @param controllerIds - list of controllerIds for targets to be assigned
|
||||
*
|
||||
*/
|
||||
@Operation(summary = "Assign target(s) to given group",
|
||||
description = "Handles the POST request of target assignment. Already assigned target will be ignored. " +
|
||||
"For complex groups use analogical method with query parameters.")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "Successfully assigned"),
|
||||
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
|
||||
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))),
|
||||
@ApiResponse(responseCode = "401", description = "The request requires user authentication."),
|
||||
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be " +
|
||||
"changed (i.e. read-only) or data volume restriction applies."),
|
||||
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource."),
|
||||
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json."),
|
||||
@ApiResponse(responseCode = "409", description = "E.g. in case an entity is created or modified by another " +
|
||||
"user in another request at the same time. You may retry your modification request."),
|
||||
@ApiResponse(responseCode = "415", description = "The request was attempt with a media-type which is not " +
|
||||
"supported by the server for this resource."),
|
||||
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts " +
|
||||
"and the client has to wait another second.")
|
||||
})
|
||||
@PutMapping(value = MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING + MgmtRestConstants.TARGET_GROUP_TARGETS_REQUEST_MAPPING)
|
||||
ResponseEntity<Void> assignTargetsToGroup(
|
||||
@PathVariable(value = "group")
|
||||
@Schema(description = "The target group to be set. Sub-grouping not allowed here, for sub-grouping use the analogical method with query parameter.")
|
||||
String group,
|
||||
@Schema(description = "List of controller ids to be assigned", example = "[\"controllerId1\", \"controllerId2\"]")
|
||||
@RequestBody List<String> controllerIds
|
||||
);
|
||||
|
||||
/**
|
||||
* Assigns targets to a given group.
|
||||
* Complex (subgroups) are allowed - e.g. Parent/Child
|
||||
*
|
||||
* @param group - target group to be assigned
|
||||
* @param controllerIds - list of controllerIds for targets to be assigned
|
||||
*
|
||||
*/
|
||||
@Operation(summary = "Assign target(s) to given group",
|
||||
description = "Handles the PUT request of assign target group." +
|
||||
"Subgroups are allowed - e.g. Parent/Child")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "Successfully assigned"),
|
||||
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
|
||||
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))),
|
||||
@ApiResponse(responseCode = "401", description = "The request requires user authentication."),
|
||||
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be " +
|
||||
"changed (i.e. read-only) or data volume restriction applies."),
|
||||
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource."),
|
||||
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json."),
|
||||
@ApiResponse(responseCode = "409", description = "E.g. in case an entity is created or modified by another " +
|
||||
"user in another request at the same time. You may retry your modification request."),
|
||||
@ApiResponse(responseCode = "415", description = "The request was attempt with a media-type which is not " +
|
||||
"supported by the server for this resource."),
|
||||
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts " +
|
||||
"and the client has to wait another second.")
|
||||
})
|
||||
@PutMapping(value = MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING + "/assigned")
|
||||
ResponseEntity<Void> assignTargetsToGroupWithSubgroups(
|
||||
@RequestParam("group")
|
||||
@Schema(description = "The target group to be set. Sub-grouping is allowed here - '/' could be used for subgroups")
|
||||
final String group,
|
||||
@Schema(description = "List of controller ids to be assigned", example = "[\"controllerId1\", \"controllerId2\"]")
|
||||
@RequestBody List<String> controllerIds
|
||||
);
|
||||
|
||||
/**
|
||||
* Assigns targets to a given group.
|
||||
* Complex (subgroups) are allowed - e.g. Parent/Child
|
||||
*
|
||||
* @param group - target group to be assigned
|
||||
* @param rsql - filter to match desired targets
|
||||
*
|
||||
*/
|
||||
@Operation(summary = "Assign target(s) to given group by rsql",
|
||||
description = "Handles the PUT request of target group assignment." +
|
||||
"Subgroups are NOT allowed here - e.g. Parent/Child")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "Successfully assigned"),
|
||||
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
|
||||
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))),
|
||||
@ApiResponse(responseCode = "401", description = "The request requires user authentication."),
|
||||
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be " +
|
||||
"changed (i.e. read-only) or data volume restriction applies."),
|
||||
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource."),
|
||||
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json."),
|
||||
@ApiResponse(responseCode = "409", description = "E.g. in case an entity is created or modified by another " +
|
||||
"user in another request at the same time. You may retry your modification request."),
|
||||
@ApiResponse(responseCode = "415", description = "The request was attempt with a media-type which is not " +
|
||||
"supported by the server for this resource."),
|
||||
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts " +
|
||||
"and the client has to wait another second.")
|
||||
})
|
||||
@PutMapping(value = MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING + "/{group}")
|
||||
ResponseEntity<Void> assignTargetsToGroupWithRsql(
|
||||
@PathVariable final String group,
|
||||
@RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SEARCH)
|
||||
@Schema(description = """
|
||||
Query fields based on the Feed Item Query Language (FIQL). See Entity Definitions for
|
||||
available fields.""")
|
||||
final String rsql
|
||||
);
|
||||
|
||||
/**
|
||||
* Unassigns targets from their groups
|
||||
*
|
||||
* @param controllerIds - list of targets to be unassigned.
|
||||
*/
|
||||
@Operation(summary = "Unassign targets from their target groups",
|
||||
description = "Handles the DELETE request to unassign the given target(s).")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "Successfully retrieved"),
|
||||
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
|
||||
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))),
|
||||
@ApiResponse(responseCode = "401", description = "The request requires user authentication."),
|
||||
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be " +
|
||||
"changed (i.e. read-only) or data volume restriction applies."),
|
||||
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource."),
|
||||
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json."),
|
||||
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts " +
|
||||
"and the client has to wait another second.")
|
||||
})
|
||||
@DeleteMapping(value = MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING + "/assigned")
|
||||
ResponseEntity<Void> unassignTargetsFromGroup(
|
||||
@RequestBody(required = false)
|
||||
@Schema(description = "List of controller ids to be unassigned from their groups", example = "[\"controllerId1\", \"controllerId2\"]")
|
||||
List<String> controllerIds
|
||||
|
||||
);
|
||||
|
||||
/**
|
||||
* Unassigns targets from their groups
|
||||
*
|
||||
* @param rsql - filter for the matching targets to be unassigned
|
||||
*/
|
||||
@Operation(summary = "Unassign targets from their target groups",
|
||||
description = "Handles the DELETE request to unassign the given target(s).")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "Successfully retrieved"),
|
||||
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
|
||||
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))),
|
||||
@ApiResponse(responseCode = "401", description = "The request requires user authentication."),
|
||||
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be " +
|
||||
"changed (i.e. read-only) or data volume restriction applies."),
|
||||
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource."),
|
||||
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json."),
|
||||
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts " +
|
||||
"and the client has to wait another second.")
|
||||
})
|
||||
@DeleteMapping(value = MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING)
|
||||
ResponseEntity<Void> unassignTargetsFromGroupByRsql(
|
||||
@RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SEARCH, required = false)
|
||||
@Schema(description = """
|
||||
Query fields based on the Feed Item Query Language (FIQL). See Entity Definitions for
|
||||
available fields.""")
|
||||
final String rsql
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @return list of all assigned target groups
|
||||
*/
|
||||
@Operation(summary = "Return all assigned target groups",
|
||||
description = "Handles the GET request of retrieving a list of all target groups.")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "Successfully retrieved"),
|
||||
@ApiResponse(responseCode = "401", description = "The request requires user authentication."),
|
||||
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be " +
|
||||
"changed (i.e. read-only) or data volume restriction applies."),
|
||||
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource."),
|
||||
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json."),
|
||||
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts " +
|
||||
"and the client has to wait another second.")
|
||||
})
|
||||
@GetMapping(value = MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING,
|
||||
produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE })
|
||||
ResponseEntity<List<String>> getTargetGroups();
|
||||
|
||||
|
||||
/**
|
||||
* Assign targets matching a rsql filter to a provided target group
|
||||
* @param group - target group to be assigned
|
||||
* @param rsqlParam - rsql filter based on Target fields
|
||||
*/
|
||||
@Operation(summary = "Assign targets matching a rsql filter to provided target group",
|
||||
description = "Handles the GET request of assigning targets matching a rsql filter to a provided target group" +
|
||||
"Subgroups are allowed - e.g. Parent/Child")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "Successfully assigned"),
|
||||
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
|
||||
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))),
|
||||
@ApiResponse(responseCode = "401", description = "The request requires user authentication."),
|
||||
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be " +
|
||||
"changed (i.e. read-only) or data volume restriction applies."),
|
||||
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource."),
|
||||
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json."),
|
||||
@ApiResponse(responseCode = "409", description = "E.g. in case an entity is created or modified by another " +
|
||||
"user in another request at the same time. You may retry your modification request."),
|
||||
@ApiResponse(responseCode = "415", description = "The request was attempt with a media-type which is not " +
|
||||
"supported by the server for this resource."),
|
||||
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts " +
|
||||
"and the client has to wait another second.")
|
||||
})
|
||||
@PutMapping(value = MgmtRestConstants.TARGET_GROUP_V1_REQUEST_MAPPING)
|
||||
ResponseEntity<Void> assignTargetsToGroup(
|
||||
@RequestParam(name = "group")
|
||||
@Schema(description = "The target group to be set. Sub-grouping is allowed here - '/' could be used for subgroups")
|
||||
final String group,
|
||||
@RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SEARCH)
|
||||
@Schema(description = """
|
||||
Query fields based on the Feed Item Query Language (FIQL). See Entity Definitions for
|
||||
available fields.""")
|
||||
String rsqlParam);
|
||||
}
|
||||
@@ -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")));
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -620,6 +620,41 @@ public interface TargetManagement {
|
||||
@PreAuthorize(HAS_AUTH_UPDATE_TARGET)
|
||||
Target assignType(@NotEmpty String controllerId, @NotNull Long targetTypeId);
|
||||
|
||||
/**
|
||||
* Finds targets by group or subgroup.
|
||||
* @param group - provided group/subgroup to filter for
|
||||
* @param withSubgroups - whether is a subgroup or not e.g. x/y/z
|
||||
* @param pageable - page parameter
|
||||
* @return all matching targets to provided group/subgroup
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET)
|
||||
Page<Target> findTargetsByGroup(@NotEmpty String group, boolean withSubgroups, @NotNull Pageable pageable);
|
||||
/**
|
||||
* Finds all the distinct target groups in the scope of a tenant
|
||||
*
|
||||
* @return list of all distinct target groups
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET)
|
||||
List<String> findGroups();
|
||||
|
||||
/**
|
||||
* Assigns the target group of the targets matching the provided rsql filter.
|
||||
*
|
||||
* @param group target group parameter
|
||||
* @param rsql rsql filter for {@link Target}
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET)
|
||||
void assignTargetGroupWithRsql(String group, @NotNull String rsql);
|
||||
|
||||
/**
|
||||
* Assigns the provided group to the targets which are in the provided list of controllerIds.
|
||||
*
|
||||
* @param group target group parameter
|
||||
* @param controllerIds list of targets
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET)
|
||||
void assignTargetsWithGroup(String group, @NotEmpty List<String> controllerIds);
|
||||
|
||||
/**
|
||||
* updates the {@link Target}.
|
||||
*
|
||||
|
||||
@@ -73,6 +73,12 @@ public interface TargetCreate {
|
||||
*/
|
||||
TargetCreate status(@NotNull TargetUpdateStatus status);
|
||||
|
||||
/**
|
||||
* @param group for setting the group of the target
|
||||
* @return updated builder instance
|
||||
*/
|
||||
TargetCreate group(String group);
|
||||
|
||||
/**
|
||||
* @return peek on current state of {@link Target} in the builder
|
||||
*/
|
||||
|
||||
@@ -70,4 +70,11 @@ public interface TargetUpdate {
|
||||
* @return updated builder instance
|
||||
*/
|
||||
TargetUpdate requestAttributes(Boolean requestAttributes);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param group for {@link Target#getGroup()}
|
||||
* @return updated builder instance
|
||||
*/
|
||||
TargetUpdate group(String group);
|
||||
}
|
||||
@@ -124,4 +124,10 @@ public interface Target extends NamedEntity {
|
||||
* {@link #getControllerAttributes()}.
|
||||
*/
|
||||
boolean isRequestControllerAttributes();
|
||||
|
||||
/**
|
||||
*
|
||||
* @return the group of the target
|
||||
*/
|
||||
String getGroup();
|
||||
}
|
||||
@@ -34,6 +34,7 @@ public class AbstractTargetUpdateCreate<T> extends AbstractNamedEntityBuilder<T>
|
||||
protected TargetUpdateStatus status;
|
||||
protected Boolean requestAttributes;
|
||||
protected Long targetTypeId;
|
||||
protected String group;
|
||||
|
||||
protected AbstractTargetUpdateCreate(final String controllerId) {
|
||||
this.controllerId = AbstractBaseEntityBuilder.strip(controllerId);
|
||||
@@ -83,6 +84,11 @@ public class AbstractTargetUpdateCreate<T> extends AbstractNamedEntityBuilder<T>
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
public T group(final String group) {
|
||||
this.group = group;
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
public String getControllerId() {
|
||||
return controllerId;
|
||||
}
|
||||
@@ -107,4 +113,6 @@ public class AbstractTargetUpdateCreate<T> extends AbstractNamedEntityBuilder<T>
|
||||
return targetTypeId;
|
||||
}
|
||||
|
||||
public Optional<String> getGroup() { return Optional.ofNullable(group); }
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE sp_target ADD COLUMN target_group VARCHAR(256);
|
||||
CREATE INDEX sp_idx_target_group ON sp_target (tenant, target_group);
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE sp_target ADD COLUMN target_group VARCHAR(256);
|
||||
CREATE INDEX sp_idx_target_group ON sp_target (tenant, target_group);
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE sp_target ADD COLUMN target_group VARCHAR(256);
|
||||
CREATE INDEX sp_idx_target_group ON sp_target (tenant, target_group);
|
||||
@@ -53,6 +53,7 @@ public class JpaTargetCreate extends AbstractTargetUpdateCreate<TargetCreate> im
|
||||
target.setAddress(address);
|
||||
target.setUpdateStatus(getStatus().orElse(TargetUpdateStatus.UNKNOWN));
|
||||
getLastTargetQuery().ifPresent(target::setLastTargetQuery);
|
||||
target.setGroup(getGroup().orElse(null));
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,9 @@ import java.util.stream.Collectors;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.CriteriaUpdate;
|
||||
import jakarta.persistence.criteria.MapJoin;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import jakarta.persistence.metamodel.MapAttribute;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
@@ -562,6 +564,57 @@ public class JpaTargetManagement implements TargetManagement {
|
||||
return targetRepository.save(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<Target> findTargetsByGroup(String group, final boolean withSubgroups, final Pageable pageable) {
|
||||
if (withSubgroups) {
|
||||
// search for eq(group) and like(group%)
|
||||
return JpaManagementHelper
|
||||
.findAllWithCountBySpec(targetRepository, List.of(TargetSpecifications.eqOrSubTargetGroup(group)), pageable);
|
||||
} else {
|
||||
return JpaManagementHelper
|
||||
.findAllWithCountBySpec(targetRepository, List.of(TargetSpecifications.eqTargetGroup(group)), pageable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> findGroups() {
|
||||
return targetRepository.findDistinctGroups();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX,
|
||||
backoff = @Backoff(delay = Constants.TX_RT_DELAY))
|
||||
public void assignTargetGroupWithRsql(String group, String rsql) {
|
||||
final Specification<JpaTarget> rsqlSpecification = RsqlUtility.getInstance().buildRsqlSpecification(rsql, TargetFields.class);
|
||||
|
||||
final CriteriaBuilder cb = entityManager.getCriteriaBuilder();
|
||||
final CriteriaUpdate<JpaTarget> criteriaUpdateQuery = cb.createCriteriaUpdate(JpaTarget.class);
|
||||
final Root<JpaTarget> root = criteriaUpdateQuery.getRoot();
|
||||
criteriaUpdateQuery.set("group", group);
|
||||
// get predicate from rsql specification using a dummy query in order to execute batch update
|
||||
final Predicate predicate = rsqlSpecification.toPredicate(root, entityManager.getCriteriaBuilder().createQuery(JpaTarget.class), cb);
|
||||
criteriaUpdateQuery.where(predicate);
|
||||
|
||||
entityManager.createQuery(criteriaUpdateQuery).executeUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX,
|
||||
backoff = @Backoff(delay = Constants.TX_RT_DELAY))
|
||||
public void assignTargetsWithGroup(String group, List<String> controllerIds) {
|
||||
final CriteriaBuilder cb = entityManager.getCriteriaBuilder();
|
||||
CriteriaUpdate<JpaTarget> criteriaQuery = cb.createCriteriaUpdate(JpaTarget.class);
|
||||
Root<JpaTarget> root = criteriaQuery.from(JpaTarget.class);
|
||||
CriteriaBuilder.In<String> in = cb.in(root.get("controllerId"));
|
||||
controllerIds.forEach(in::value);
|
||||
|
||||
entityManager.createQuery(criteriaQuery.set("group", group).where(in)).executeUpdate();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX,
|
||||
|
||||
@@ -154,6 +154,11 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw
|
||||
// set default request controller attributes to true, because we want to request them the first time
|
||||
private boolean requestControllerAttributes = true;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
@Column(name = "target_group")
|
||||
private String group;
|
||||
|
||||
// actually it is OneToOne - but lazy loading is not supported for OneToOne (at least for hibernate 6.6.2)
|
||||
@OneToMany(fetch = FetchType.LAZY, mappedBy = "target", cascade = { CascadeType.ALL }, orphanRemoval = true)
|
||||
@PrimaryKeyJoinColumn
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
package org.eclipse.hawkbit.repository.jpa.repository;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
@@ -96,4 +97,11 @@ public interface TargetRepository extends BaseEntityRepository<JpaTarget> {
|
||||
@Transactional
|
||||
@Query("DELETE FROM JpaTarget t WHERE t.tenant = :tenant")
|
||||
void deleteByTenant(@Param("tenant") String tenant);
|
||||
|
||||
/**
|
||||
* Finds all available distinct target groups
|
||||
* @return all target groups
|
||||
*/
|
||||
@Query(value = "SELECT DISTINCT target_group FROM sp_target WHERE target_group IS NOT NULL", nativeQuery = true)
|
||||
List<String> findDistinctGroups();
|
||||
}
|
||||
@@ -192,6 +192,22 @@ public final class TargetSpecifications {
|
||||
};
|
||||
}
|
||||
|
||||
public static Specification<JpaTarget> eqTargetGroup(final String targetGroup) {
|
||||
return (targetRoot, query, criteriaBuilder) -> {
|
||||
final String groupTextToLower = targetGroup.toLowerCase();
|
||||
return criteriaBuilder.equal(criteriaBuilder.lower(targetRoot.get(JpaTarget_.group)), groupTextToLower);
|
||||
};
|
||||
}
|
||||
|
||||
public static Specification<JpaTarget> eqOrSubTargetGroup(final String targetGroupSearch) {
|
||||
return (targetRoot, query, criteriaBuilder) -> {
|
||||
final String searchTextToLower = targetGroupSearch.toLowerCase();
|
||||
return criteriaBuilder.or(
|
||||
criteriaBuilder.equal(criteriaBuilder.lower(targetRoot.get(JpaTarget_.group)), searchTextToLower),
|
||||
criteriaBuilder.like(criteriaBuilder.lower(targetRoot.get(JpaTarget_.group)), searchTextToLower.concat("/%")));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Specification} for retrieving {@link Target}s by "like controllerId".
|
||||
*
|
||||
|
||||
@@ -21,6 +21,7 @@ public interface Constants {
|
||||
String VERSION = "Version";
|
||||
String VENDOR = "Vendor";
|
||||
String TYPE = "Type";
|
||||
String GROUP = "Group";
|
||||
String CREATED_BY = "Created by";
|
||||
String CREATED_AT = "Created at";
|
||||
String LAST_MODIFIED_BY = "Last modified by";
|
||||
|
||||
@@ -345,6 +345,7 @@ public class TargetView extends TableView<MgmtTarget, String> {
|
||||
private final TextField lastModifiedAt = Utils.textField(Constants.LAST_MODIFIED_AT);
|
||||
private final TextField securityToken = Utils.textField(Constants.SECURITY_TOKEN);
|
||||
private final TextField lastPoll = Utils.textField(Constants.LAST_POLL);
|
||||
private final TextField group = Utils.textField(Constants.GROUP);
|
||||
private final TextArea targetAttributes = new TextArea(Constants.ATTRIBUTES);
|
||||
private transient MgmtTarget target;
|
||||
|
||||
@@ -355,7 +356,7 @@ public class TargetView extends TableView<MgmtTarget, String> {
|
||||
description,
|
||||
createdBy, createdAt,
|
||||
lastModifiedBy, lastModifiedAt,
|
||||
securityToken, lastPoll, targetAttributes
|
||||
securityToken, lastPoll, targetAttributes, group
|
||||
)
|
||||
.forEach(field -> {
|
||||
field.setReadOnly(true);
|
||||
@@ -378,6 +379,7 @@ public class TargetView extends TableView<MgmtTarget, String> {
|
||||
lastModifiedBy.setValue(target.getLastModifiedBy());
|
||||
lastModifiedAt.setValue(new Date(target.getLastModifiedAt()).toString());
|
||||
securityToken.setValue(target.getSecurityToken());
|
||||
group.setValue(target.getGroup() != null ? target.getGroup() : "");
|
||||
|
||||
final MgmtPollStatus pollStatus = target.getPollStatus();
|
||||
lastPoll.setValue(pollStatus == null ? NOT_AVAILABLE_NULL : new Date(pollStatus.getLastRequestAt()).toString());
|
||||
|
||||
Reference in New Issue
Block a user