From e154e1b18abdb57690b91527109ed1077debeaec Mon Sep 17 00:00:00 2001 From: Avgustin Marinov Date: Wed, 22 Oct 2025 09:57:45 +0300 Subject: [PATCH] [#2429] Add completeness property for software modules (#2765) * add `min artifacts` requirement on the Software Module Type level for Software Module completeness * removed `complete` Distribution Set property from DB - calculated runtime * Distribution Set and Software Module completeness is calcualted on demand in memory (TODO: implement cache) * locking of Software Module now requires the software module to be `completed` * removed 'complete' search field for DistributionSet type. Still keep (DEPRECATED) limited support for search with 'complete' - only on the first level of expression and with AND. I.e. complete==true, complete==false and id=in=(1, 3) is suppoted, while complete==false or id=in=(1, 3) and id=in(1, 3) and (type==os and complete==true) are not Signed-off-by: Avgustin Marinov --- .../repository/DistributionSetFields.java | 1 - .../distributionset/MgmtDistributionSet.java | 9 +- .../softwaremodule/MgmtSoftwareModule.java | 3 + .../MgmtSoftwareModuleType.java | 7 +- ...MgmtSoftwareModuleTypeRequestBodyPost.java | 3 + .../resource/MgmtDistributionSetResource.java | 4 + .../mapper/MgmtDistributionSetMapper.java | 7 +- .../mapper/MgmtSoftwareModuleMapper.java | 26 ++--- .../mapper/MgmtSoftwareModuleTypeMapper.java | 39 ++++---- .../SoftwareModuleTypeManagement.java | 2 + .../entity/DistributionSetUpdatedEvent.java | 11 +-- .../IncompleteSoftwareModuleException.java | 40 ++++++++ .../repository/model/DistributionSet.java | 14 +-- .../repository/model/DistributionSetType.java | 6 -- .../repository/model/SoftwareModule.java | 7 ++ .../repository/model/SoftwareModuleType.java | 7 +- .../V1_12_34__sm_type_min_artifacts__H2.sql | 4 + ...V1_12_34__sm_type_min_artifacts__MYSQL.sql | 4 + ..._35__sm_type_min_artifacts__POSTGRESQL.sql | 4 + .../hawkbit/repository/jpa/ql/QLSupport.java | 18 ++-- .../JpaDistributionSetManagement.java | 77 +++++++++++++-- .../JpaSoftwareModuleManagement.java | 4 + .../jpa/model/JpaDistributionSet.java | 43 +++++---- .../jpa/model/JpaDistributionSetType.java | 9 -- .../jpa/model/JpaSoftwareModule.java | 12 +++ .../jpa/model/JpaSoftwareModuleType.java | 5 + .../DistributionSetSpecification.java | 20 ---- .../DistributionSetUpdatedEventTest.java | 2 +- .../management/DeploymentManagementTest.java | 8 +- .../DistributionSetManagementTest.java | 96 ++++++++++++++++++- .../DistributionSetTypeManagementTest.java | 12 --- .../SoftwareModuleManagementTest.java | 28 ++++++ 32 files changed, 377 insertions(+), 155 deletions(-) create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/IncompleteSoftwareModuleException.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa-flyway/src/main/resources/db/migration/H2/V1_12_34__sm_type_min_artifacts__H2.sql create mode 100644 hawkbit-repository/hawkbit-repository-jpa-flyway/src/main/resources/db/migration/MYSQL/V1_12_34__sm_type_min_artifacts__MYSQL.sql create mode 100644 hawkbit-repository/hawkbit-repository-jpa-flyway/src/main/resources/db/migration/POSTGRESQL/V1_12_35__sm_type_min_artifacts__POSTGRESQL.sql diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/DistributionSetFields.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/DistributionSetFields.java index 47be44205..c66b91e53 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/DistributionSetFields.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/DistributionSetFields.java @@ -32,7 +32,6 @@ public enum DistributionSetFields implements QueryField { LASTMODIFIEDAT("lastModifiedAt"), LASTMODIFIEDBY("lastModifiedBy"), VERSION("version"), - COMPLETE("complete"), MODULE("modules", SoftwareModuleFields.ID.getJpaEntityFieldName(), SoftwareModuleFields.NAME.getJpaEntityFieldName()), TAG("tags", "name"), METADATA("metadata"), diff --git a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtDistributionSet.java b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtDistributionSet.java index 3f3ce6eff..930b72637 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtDistributionSet.java +++ b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtDistributionSet.java @@ -147,11 +147,6 @@ public class MgmtDistributionSet extends MgmtNamedEntity { example = "OS (FW) mandatory, runtime (FW) and app (SW) optional") private String typeName; - @Schema(description = """ - True of the distribution set software module setup is complete as defined by the - distribution set type""", example = "true") - private Boolean complete; - @Schema(description = "If the distribution set is locked", example = "true") private boolean locked; @@ -167,4 +162,8 @@ public class MgmtDistributionSet extends MgmtNamedEntity { private boolean requiredMigrationStep; private List modules = new ArrayList<>(); + + @Schema(description = "True of the distribution set software module setup is complete as defined by the distribution set type", + example = "true") + private Boolean complete; } \ No newline at end of file diff --git a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/softwaremodule/MgmtSoftwareModule.java b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/softwaremodule/MgmtSoftwareModule.java index 63e719fb3..613194cc5 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/softwaremodule/MgmtSoftwareModule.java +++ b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/softwaremodule/MgmtSoftwareModule.java @@ -93,4 +93,7 @@ public class MgmtSoftwareModule extends MgmtNamedEntity { @Schema(description = "If the software module is deleted", example = "false") private boolean deleted; + + @Schema(description = "True of the software module has sufficient number of artifacts", example = "true") + private Boolean complete; } \ No newline at end of file diff --git a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/softwaremoduletype/MgmtSoftwareModuleType.java b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/softwaremoduletype/MgmtSoftwareModuleType.java index 441a4eb44..19c5e2d01 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/softwaremoduletype/MgmtSoftwareModuleType.java +++ b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/softwaremoduletype/MgmtSoftwareModuleType.java @@ -40,6 +40,7 @@ import org.eclipse.hawkbit.mgmt.json.model.MgmtTypeEntity; "description" : "Updated description.", "key" : "application", "maxAssignments" : 2147483647, + "minArtifacts": 1, "deleted" : false, "_links" : { "self" : { @@ -54,7 +55,9 @@ public class MgmtSoftwareModuleType extends MgmtTypeEntity { @Schema(description = "The technical identifier of the entity", example = "83") private Long id; - @Schema(description = "Software modules of that type can be assigned at this maximum number " + - "(e.g. operating system only once)", example = "1") + @Schema(description = "The minimum number of artifacts a software module if this type should have in order to be completed", example = "1") + private int minArtifacts; + + @Schema(description = "Software modules of this type can be assigned at this maximum number (e.g. operating system only once)", example = "1") private int maxAssignments; } \ No newline at end of file diff --git a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/softwaremoduletype/MgmtSoftwareModuleTypeRequestBodyPost.java b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/softwaremoduletype/MgmtSoftwareModuleTypeRequestBodyPost.java index 88d8828d5..71d382853 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/softwaremoduletype/MgmtSoftwareModuleTypeRequestBodyPost.java +++ b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/softwaremoduletype/MgmtSoftwareModuleTypeRequestBodyPost.java @@ -33,6 +33,9 @@ public class MgmtSoftwareModuleTypeRequestBodyPost extends MgmtSoftwareModuleTyp @Schema(example = "Example key") private String key; + @Schema(description = "The minimum number of artifacts a software module if this type should have in order to be completed", example = "1") + private int minArtifacts; + @JsonProperty(required = true) @Schema(example = "1") private int maxAssignments; diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java index 2121e0f7f..b357a06ce 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java @@ -45,6 +45,7 @@ import org.eclipse.hawkbit.mgmt.rest.resource.mapper.MgmtRestModelMapper; import org.eclipse.hawkbit.mgmt.rest.resource.mapper.MgmtSoftwareModuleMapper; import org.eclipse.hawkbit.mgmt.rest.resource.mapper.MgmtTargetFilterQueryMapper; import org.eclipse.hawkbit.mgmt.rest.resource.mapper.MgmtTargetMapper; +import org.eclipse.hawkbit.mgmt.rest.resource.util.LogUtility; import org.eclipse.hawkbit.mgmt.rest.resource.util.PagingUtility; import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.DistributionSetInvalidationManagement; @@ -116,6 +117,9 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi { @Override public ResponseEntity> getDistributionSets( final String rsqlParam, final int pagingOffsetParam, final int pagingLimitParam, final String sortParam) { + if (rsqlParam != null && rsqlParam.toLowerCase().contains("complete")) { + LogUtility.logDeprecated("Usage of MgmtDistributionSetResource.getActions with 'complete': 'complete' distribution set search field may be removed."); + } final Pageable pageable = PagingUtility.toPageable(pagingOffsetParam, pagingLimitParam, sanitizeDistributionSetSortParam(sortParam)); final Page findDsPage; if (rsqlParam != null) { diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtDistributionSetMapper.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtDistributionSetMapper.java index 6eb20b3c8..6f183d0fc 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtDistributionSetMapper.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtDistributionSetMapper.java @@ -18,7 +18,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -33,9 +32,7 @@ import org.eclipse.hawkbit.mgmt.rest.api.MgmtDistributionSetTypeRestApi; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.DistributionSetTagManagement; -import org.eclipse.hawkbit.repository.DistributionSetTypeManagement; import org.eclipse.hawkbit.repository.SoftwareModuleManagement; -import org.eclipse.hawkbit.repository.SystemManagement; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; @@ -60,7 +57,7 @@ public class MgmtDistributionSetMapper { public List fromRequest( final Collection sets, final String defaultDsKey, final Map dsTypeKeyToDsType) { - return sets.stream().map(dsRest -> { + return sets.stream(). map(dsRest -> { final Set modules = new HashSet<>(); if (dsRest.getOs() != null) { modules.add(dsRest.getOs().getId()); @@ -195,7 +192,7 @@ public class MgmtDistributionSetMapper { } private Set findSoftwareModuleWithExceptionIfNotFound(final Set softwareModuleIds) { - if (CollectionUtils.isEmpty(softwareModuleIds)) { + if (CollectionUtils.isEmpty(softwareModuleIds)) { return Collections.emptySet(); } diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtSoftwareModuleMapper.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtSoftwareModuleMapper.java index c999fc9cd..e96b003f0 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtSoftwareModuleMapper.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtSoftwareModuleMapper.java @@ -34,6 +34,7 @@ import org.eclipse.hawkbit.mgmt.rest.api.MgmtSoftwareModuleTypeRestApi; import org.eclipse.hawkbit.mgmt.rest.resource.MgmtDownloadArtifactResource; import org.eclipse.hawkbit.mgmt.rest.resource.MgmtSoftwareModuleResource; import org.eclipse.hawkbit.repository.SoftwareModuleManagement; +import org.eclipse.hawkbit.repository.SoftwareModuleManagement.Create; import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement; import org.eclipse.hawkbit.repository.SystemManagement; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; @@ -63,17 +64,15 @@ public final class MgmtSoftwareModuleMapper { if (metadata == null) { return Collections.emptyMap(); } - return metadata.stream().collect(Collectors.toMap( MgmtSoftwareModuleMetadata::getKey, metadataRest -> new MetadataValueCreate(metadataRest.getValue(), metadataRest.isTargetVisible()))); } - public List smFromRequest(final Collection smsRest) { + public List smFromRequest(final Collection smsRest) { if (smsRest == null) { return Collections.emptyList(); } - return smsRest.stream().map(this::fromRequest).toList(); } @@ -81,7 +80,6 @@ public final class MgmtSoftwareModuleMapper { if (softwareModules == null) { return Collections.emptyList(); } - return new ResponseList<>(softwareModules.stream().map(MgmtSoftwareModuleMapper::toResponse).toList()); } @@ -89,7 +87,6 @@ public final class MgmtSoftwareModuleMapper { if (metadata == null) { return Collections.emptyList(); } - return metadata.entrySet().stream().map(e -> toResponseSwMetadata(e.getKey(), e.getValue())).toList(); } @@ -105,7 +102,6 @@ public final class MgmtSoftwareModuleMapper { if (softwareModule == null) { return null; } - final MgmtSoftwareModule response = new MgmtSoftwareModule(); MgmtRestModelMapper.mapNamedToNamed(response, softwareModule); response.setId(softwareModule.getId()); @@ -116,10 +112,8 @@ public final class MgmtSoftwareModuleMapper { response.setLocked(softwareModule.isLocked()); response.setDeleted(softwareModule.isDeleted()); response.setEncrypted(softwareModule.isEncrypted()); - - response.add(linkTo(methodOn(MgmtSoftwareModuleRestApi.class).getSoftwareModule(response.getId())) - .withSelfRel().expand()); - + response.setComplete(softwareModule.isComplete()); + response.add(linkTo(methodOn(MgmtSoftwareModuleRestApi.class).getSoftwareModule(response.getId())).withSelfRel().expand()); return response; } @@ -136,16 +130,11 @@ public final class MgmtSoftwareModuleMapper { final MgmtArtifact artifactRest = new MgmtArtifact(); artifactRest.setId(artifact.getId()); artifactRest.setSize(artifact.getSize()); - artifactRest.setHashes( - new MgmtArtifactHash(artifact.getSha1Hash(), artifact.getMd5Hash(), artifact.getSha256Hash())); - + artifactRest.setHashes(new MgmtArtifactHash(artifact.getSha1Hash(), artifact.getMd5Hash(), artifact.getSha256Hash())); artifactRest.setProvidedFilename(artifact.getFilename()); - MgmtRestModelMapper.mapBaseToBase(artifactRest, artifact); - artifactRest.add(linkTo(methodOn(MgmtSoftwareModuleRestApi.class) .getArtifact(artifact.getSoftwareModule().getId(), artifact.getId(), null)).withSelfRel().expand()); - return artifactRest; } @@ -165,8 +154,8 @@ public final class MgmtSoftwareModuleMapper { urls.forEach(entry -> response.add(Link.of(entry.ref()).withRel(entry.rel()).expand())); } - private SoftwareModuleManagement.Create fromRequest(final MgmtSoftwareModuleRequestBodyPost smsRest) { - return SoftwareModuleManagement.Create.builder() + private Create fromRequest(final MgmtSoftwareModuleRequestBodyPost smsRest) { + return Create.builder() .type(getSoftwareModuleTypeFromKeyString(smsRest.getType())) .name(smsRest.getName()).version(smsRest.getVersion()).description(smsRest.getDescription()).vendor(smsRest.getVendor()) .encrypted(smsRest.isEncrypted()) @@ -177,7 +166,6 @@ public final class MgmtSoftwareModuleMapper { if (type == null) { throw new ValidationException("type cannot be null"); } - return softwareModuleTypeManagement.findByKey(type.trim()) .orElseThrow(() -> new EntityNotFoundException(SoftwareModuleType.class, type.trim())); } diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtSoftwareModuleTypeMapper.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtSoftwareModuleTypeMapper.java index cb6faffa2..ca6033669 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtSoftwareModuleTypeMapper.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtSoftwareModuleTypeMapper.java @@ -13,7 +13,6 @@ import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; import java.util.Collection; -import java.util.Collections; import java.util.List; import lombok.AccessLevel; @@ -21,7 +20,7 @@ import lombok.NoArgsConstructor; import org.eclipse.hawkbit.mgmt.json.model.softwaremoduletype.MgmtSoftwareModuleType; import org.eclipse.hawkbit.mgmt.json.model.softwaremoduletype.MgmtSoftwareModuleTypeRequestBodyPost; import org.eclipse.hawkbit.mgmt.rest.api.MgmtSoftwareModuleTypeRestApi; -import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement; +import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement.Create; import org.eclipse.hawkbit.repository.model.SoftwareModuleType; import org.eclipse.hawkbit.rest.json.model.ResponseList; @@ -31,41 +30,37 @@ import org.eclipse.hawkbit.rest.json.model.ResponseList; @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class MgmtSoftwareModuleTypeMapper { - public static List smFromRequest( - final Collection smTypesRest) { - if (smTypesRest == null) { - return Collections.emptyList(); + public static List smFromRequest(final Collection request) { + if (request == null) { + return List.of(); } - - return smTypesRest.stream().map(MgmtSoftwareModuleTypeMapper::fromRequest).toList(); + return request.stream().map(MgmtSoftwareModuleTypeMapper::fromRequest).toList(); } public static List toTypesResponse(final Collection types) { if (types == null) { - return Collections.emptyList(); + return List.of(); } - return new ResponseList<>(types.stream().map(MgmtSoftwareModuleTypeMapper::toResponse).toList()); } public static MgmtSoftwareModuleType toResponse(final SoftwareModuleType type) { final MgmtSoftwareModuleType result = new MgmtSoftwareModuleType(); - MgmtRestModelMapper.mapTypeToType(result, type); - result.setMaxAssignments(type.getMaxAssignments()); - result.setId(type.getId()); - - result.add(linkTo(methodOn(MgmtSoftwareModuleTypeRestApi.class).getSoftwareModuleType(result.getId())) - .withSelfRel().expand()); - + result + .setId(type.getId()) + .setMinArtifacts(type.getMinArtifacts()) + .setMaxAssignments(type.getMaxAssignments()); + result.add(linkTo(methodOn(MgmtSoftwareModuleTypeRestApi.class).getSoftwareModuleType(result.getId())).withSelfRel().expand()); return result; } - private static SoftwareModuleTypeManagement.Create fromRequest(final MgmtSoftwareModuleTypeRequestBodyPost smsRest) { - return SoftwareModuleTypeManagement.Create.builder() - .key(smsRest.getKey()).name(smsRest.getName()) - .description(smsRest.getDescription()).colour(smsRest.getColour()) - .maxAssignments(smsRest.getMaxAssignments()) + private static Create fromRequest(final MgmtSoftwareModuleTypeRequestBodyPost request) { + return Create.builder() + .key(request.getKey()).name(request.getName()) + .description(request.getDescription()).colour(request.getColour()) + .minArtifacts(request.getMinArtifacts()) + .maxAssignments(request.getMaxAssignments()) .build(); } } \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleTypeManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleTypeManagement.java index 3ff548a2f..d69353088 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleTypeManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleTypeManagement.java @@ -63,6 +63,8 @@ public interface SoftwareModuleTypeManagement @Builder.Default private int maxAssignments = 1; + @Builder.Default + private int minArtifacts; } @SuperBuilder diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/entity/DistributionSetUpdatedEvent.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/entity/DistributionSetUpdatedEvent.java index 29c65ca87..9da624682 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/entity/DistributionSetUpdatedEvent.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/entity/DistributionSetUpdatedEvent.java @@ -28,16 +28,7 @@ public class DistributionSetUpdatedEvent extends RemoteEntityEventtrue if {@link DistributionSet} is after the update {@link DistributionSet#isComplete()} - */ - public DistributionSetUpdatedEvent(final DistributionSet distributionSet, final boolean complete) { + public DistributionSetUpdatedEvent(final DistributionSet distributionSet) { super(distributionSet); - this.complete = complete; } } \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/IncompleteSoftwareModuleException.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/IncompleteSoftwareModuleException.java new file mode 100644 index 000000000..96458bb64 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/IncompleteSoftwareModuleException.java @@ -0,0 +1,40 @@ +/** + * 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.repository.exception; + +import java.io.Serial; + +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.eclipse.hawkbit.exception.AbstractServerRtException; +import org.eclipse.hawkbit.exception.SpServerError; + +/** + * Thrown if a software module is being locked while incomplete (i.e. not enough artifacts are assigned). + */ +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class IncompleteSoftwareModuleException extends AbstractServerRtException { + + @Serial + private static final long serialVersionUID = 1L; + + public IncompleteSoftwareModuleException() { + super(SpServerError.SP_DS_INCOMPLETE); + } + + public IncompleteSoftwareModuleException(final Throwable cause) { + super(SpServerError.SP_DS_INCOMPLETE, cause); + } + + public IncompleteSoftwareModuleException(final String message) { + super(SpServerError.SP_DS_INCOMPLETE, message); + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSet.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSet.java index 064ae03d8..efdb24fac 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSet.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSet.java @@ -39,12 +39,6 @@ public interface DistributionSet extends NamedVersionedEntity { */ Set getModules(); - /** - * @return true if all defined {@link DistributionSetType#getMandatoryModuleTypes()} of {@link #getType()} are present in - * this {@link DistributionSet}. - */ - boolean isComplete(); - /** * @return true if this {@link DistributionSet} is locked. If so it's 'functional' properties (e.g. software modules) could not * be modified anymore. @@ -66,4 +60,12 @@ public interface DistributionSet extends NamedVersionedEntity { * active and not automatically canceled if overridden by a newer update. */ boolean isRequiredMigrationStep(); + + /** + * Returns if the distribution set could be assumed as completed. I.e. all requirements (e.g. mandatory software module types) are satisfied. + * + * @return true if all defined {@link DistributionSetType#getMandatoryModuleTypes()} of {@link #getType()} are present in + * this {@link DistributionSet}. + */ + boolean isComplete(); } \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetType.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetType.java index 2bbff226c..659e16fad 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetType.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetType.java @@ -29,12 +29,6 @@ public interface DistributionSetType extends Type { */ Set getOptionalModuleTypes(); - /** - * @param distributionSet to check for completeness - * @return true if the all mandatory software module types are in the system. - */ - boolean checkComplete(DistributionSet distributionSet); - /** * Checks if the given {@link SoftwareModuleType} is in this {@link DistributionSetType}. * diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/SoftwareModule.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/SoftwareModule.java index c008a5bf8..1d0842d39 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/SoftwareModule.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/SoftwareModule.java @@ -70,6 +70,13 @@ public interface SoftwareModule extends NamedVersionedEntity { */ boolean isDeleted(); + /** + * Returns if the software module could be assumed as completed. I.e. all requirements (e.g. min artifacts) are satisfied. + * + * @return true if artifacts are more or equals to {@link SoftwareModuleType#getMinArtifacts()} if the software module type. + */ + boolean isComplete(); + /** * @param artifactId to look for * @return found {@link Artifact} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/SoftwareModuleType.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/SoftwareModuleType.java index 0521b0044..f4dab332f 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/SoftwareModuleType.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/SoftwareModuleType.java @@ -16,7 +16,12 @@ package org.eclipse.hawkbit.repository.model; public interface SoftwareModuleType extends Type { /** - * @return maximum assignments of an {@link SoftwareModule} of this type to a {@link DistributionSet}. + * @return thet minimum number of artifacts a {@link SoftwareModule} of this type should have in order to be completed. + */ + int getMinArtifacts(); + + /** + * @return the maximum assignments of a {@link SoftwareModule} of this type to a {@link DistributionSet}. */ int getMaxAssignments(); } diff --git a/hawkbit-repository/hawkbit-repository-jpa-flyway/src/main/resources/db/migration/H2/V1_12_34__sm_type_min_artifacts__H2.sql b/hawkbit-repository/hawkbit-repository-jpa-flyway/src/main/resources/db/migration/H2/V1_12_34__sm_type_min_artifacts__H2.sql new file mode 100644 index 000000000..cb4664375 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa-flyway/src/main/resources/db/migration/H2/V1_12_34__sm_type_min_artifacts__H2.sql @@ -0,0 +1,4 @@ +ALTER TABLE sp_software_module_type ADD COLUMN min_artifacts integer default 0 NOT NULL; +DROP INDEX sp_idx_distribution_set_01; +CREATE INDEX sp_idx_distribution_set_01 ON sp_distribution_set (tenant, deleted); +ALTER TABLE sp_distribution_set DROP COLUMN complete; \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa-flyway/src/main/resources/db/migration/MYSQL/V1_12_34__sm_type_min_artifacts__MYSQL.sql b/hawkbit-repository/hawkbit-repository-jpa-flyway/src/main/resources/db/migration/MYSQL/V1_12_34__sm_type_min_artifacts__MYSQL.sql new file mode 100644 index 000000000..d3e5aafed --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa-flyway/src/main/resources/db/migration/MYSQL/V1_12_34__sm_type_min_artifacts__MYSQL.sql @@ -0,0 +1,4 @@ +ALTER TABLE sp_software_module_type ADD COLUMN min_artifacts integer default 0 NOT NULL; +ALTER TABLE sp_distribution_set DROP INDEX sp_idx_distribution_set_01; +CREATE INDEX sp_idx_distribution_set_01 ON sp_distribution_set (tenant, deleted); +ALTER TABLE sp_distribution_set DROP COLUMN complete; \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa-flyway/src/main/resources/db/migration/POSTGRESQL/V1_12_35__sm_type_min_artifacts__POSTGRESQL.sql b/hawkbit-repository/hawkbit-repository-jpa-flyway/src/main/resources/db/migration/POSTGRESQL/V1_12_35__sm_type_min_artifacts__POSTGRESQL.sql new file mode 100644 index 000000000..0d7213632 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa-flyway/src/main/resources/db/migration/POSTGRESQL/V1_12_35__sm_type_min_artifacts__POSTGRESQL.sql @@ -0,0 +1,4 @@ +ALTER TABLE sp_software_module_type ADD COLUMN min_artifacts integer default 0 NOT NULL; +DROP INDEX sp_idx_distribution_set_01; +CREATE INDEX sp_idx_distribution_set_01 ON sp_distribution_set USING BTREE (tenant, deleted); +ALTER TABLE sp_distribution_set DROP COLUMN complete; \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa-ql/src/main/java/org/eclipse/hawkbit/repository/jpa/ql/QLSupport.java b/hawkbit-repository/hawkbit-repository-jpa-ql/src/main/java/org/eclipse/hawkbit/repository/jpa/ql/QLSupport.java index 0a181a4bd..c1f4b53c9 100644 --- a/hawkbit-repository/hawkbit-repository-jpa-ql/src/main/java/org/eclipse/hawkbit/repository/jpa/ql/QLSupport.java +++ b/hawkbit-repository/hawkbit-repository-jpa-ql/src/main/java/org/eclipse/hawkbit/repository/jpa/ql/QLSupport.java @@ -154,6 +154,10 @@ public class QLSupport implements ApplicationListener { this.virtualPropertyResolver = virtualPropertyResolver; } + public & QueryField> Node parse(final String query, final Class queryFieldType) { + return parser.parse(ignoreCase || caseInsensitiveDB ? query.toLowerCase() : query, queryFieldType); + } + /** * Builds a JPA {@link Specification} which corresponds with the given RSQL query. The specification can be used to filter for JPA entities * with the given RSQL query. @@ -168,16 +172,20 @@ public class QLSupport implements ApplicationListener { public & QueryField, T> Specification buildSpec(final String query, final Class queryFieldType) { if (specBuilder == SpecBuilder.G3) { return new SpecificationBuilder(ignoreCase && !caseInsensitiveDB , database) - .specification(parseAndTransform(query, queryFieldType, ignoreCase || caseInsensitiveDB)); + .specification(transform(parse(query, queryFieldType), queryFieldType)); } else { return new SpecificationBuilderLegacy(queryFieldType, virtualPropertyResolver, database).specification(query); } } + public & QueryField, T> Specification buildSpec(final Node query, final Class queryFieldType) { + return new SpecificationBuilder(ignoreCase && !caseInsensitiveDB , database) + .specification(transform(query, queryFieldType)); + } + @SuppressWarnings("java:S1117") // it is again ignoreCase public & QueryField> EntityMatcher entityMatcher(final String query, final Class queryFieldType) { - final boolean ignoreCase = this.ignoreCase || caseInsensitiveDB; // sync with DB and case sensitivity requirements - return EntityMatcher.of(parseAndTransform(query, queryFieldType, ignoreCase), ignoreCase); + return EntityMatcher.of(transform(parse(query, queryFieldType), queryFieldType), ignoreCase || caseInsensitiveDB); } /** @@ -195,9 +203,7 @@ public class QLSupport implements ApplicationListener { buildSpec(query, queryFieldType).toPredicate(criteriaQuery.from((Class) jpaType), criteriaQuery, criteriaBuilder); } - private & QueryField> Node parseAndTransform( - final String query, final Class queryFieldType, final boolean ignoreCase) { - Node node = parser.parse(ignoreCase ? query.toLowerCase() : query, queryFieldType); + private & QueryField> Node transform(Node node, final Class queryFieldType) { for (final NodeTransformer transformer : nodeTransformers) { node = transformer.transform(node, queryFieldType); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDistributionSetManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDistributionSetManagement.java index d0099e6ac..e18b0d576 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDistributionSetManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDistributionSetManagement.java @@ -13,18 +13,21 @@ import static org.eclipse.hawkbit.repository.jpa.configuration.Constants.TX_RT_D import static org.eclipse.hawkbit.repository.jpa.configuration.Constants.TX_RT_MAX; import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.IMPLICIT_LOCK_ENABLED; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; import jakarta.persistence.EntityManager; +import lombok.extern.slf4j.Slf4j; import org.eclipse.hawkbit.repository.DistributionSetFields; import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.DistributionSetTagManagement; @@ -36,17 +39,20 @@ import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException; import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; import org.eclipse.hawkbit.repository.exception.InvalidDistributionSetException; +import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; import org.eclipse.hawkbit.repository.jpa.JpaManagementHelper; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetTag; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet_; import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModule; +import org.eclipse.hawkbit.repository.jpa.ql.Node; +import org.eclipse.hawkbit.repository.jpa.ql.QLSupport; import org.eclipse.hawkbit.repository.jpa.repository.ActionRepository; import org.eclipse.hawkbit.repository.jpa.repository.DistributionSetRepository; import org.eclipse.hawkbit.repository.jpa.repository.DistributionSetTagRepository; import org.eclipse.hawkbit.repository.jpa.repository.SoftwareModuleRepository; import org.eclipse.hawkbit.repository.jpa.repository.TargetFilterQueryRepository; -import org.eclipse.hawkbit.repository.jpa.ql.QLSupport; +import org.eclipse.hawkbit.repository.jpa.rsql.RsqlParser; import org.eclipse.hawkbit.repository.jpa.specifications.DistributionSetSpecification; import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper; import org.eclipse.hawkbit.repository.model.DistributionSet; @@ -58,7 +64,9 @@ import org.eclipse.hawkbit.utils.TenantConfigHelper; import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; import org.springframework.dao.ConcurrencyFailureException; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; @@ -67,8 +75,10 @@ import org.springframework.util.ObjectUtils; @Service @ConditionalOnBooleanProperty(prefix = "hawkbit.jpa", name = { "enabled", "distribution-set-management" }, matchIfMissing = true) +@Slf4j public class JpaDistributionSetManagement - extends AbstractJpaRepositoryWithMetadataManagement + extends + AbstractJpaRepositoryWithMetadataManagement implements DistributionSetManagement { private final DistributionSetTagManagement distributionSetTagManagement; @@ -104,6 +114,46 @@ public class JpaDistributionSetManagement this.repositoryProperties = repositoryProperties; } + private static final String COMPLETE = "complete"; + + @Override + @SuppressWarnings("java:S3776") // java:S3776 - just too complex + public Page findByRsql(final String rsql, final Pageable pageable) { + if (rsql != null && rsql.toLowerCase().contains(COMPLETE)) { + // limited support for 'complete' - could be removed in future + final Node node = RsqlParser.parse(rsql); + final Specification notDeleted = (root, query, cb) -> cb.equal(root.get(DELETED), false); + final List> specList = new ArrayList<>(); + specList.add(notDeleted); + final AtomicReference completedComparison = new AtomicReference<>(); + if (node instanceof Node.Comparison comparison && COMPLETE.equalsIgnoreCase(comparison.getKey())) { + // all not deleted, won't add anything to spec + completedComparison.set(comparison); + } else if (node instanceof Node.Logical logical && logical.getOp() == Node.Logical.Operator.AND) { + final List sanitizedChildren = new ArrayList<>(); + logical.getChildren().forEach(child -> { + if (child instanceof Node.Comparison comparison && COMPLETE.equalsIgnoreCase(comparison.getKey())) { + if (completedComparison.get() != null) { + throw new RSQLParameterSyntaxException("Multiple 'complete' comparisons are not supported"); + } + completedComparison.set(comparison); + } else { + sanitizedChildren.add(child); + } + }); + specList.add(QLSupport.getInstance() + .buildSpec(new Node.Logical(Node.Logical.Operator.AND, sanitizedChildren), DistributionSetFields.class)); + } + if (completedComparison.get() != null) { // really a comparison + log.warn("Usage of 'complete' in RSQL is deprecated and will be removed in future: {}", node); + final boolean completed = completeComparison(completedComparison); + return filter(JpaManagementHelper.findAllWithCountBySpec(jpaRepository, specList, pageable), completed); + } + } + + return super.findByRsql(rsql, pageable); + } + @Override public JpaDistributionSet update(final Update update) { final JpaDistributionSet updated = super.update(update); @@ -199,7 +249,7 @@ public class JpaDistributionSetManagement if (distributionSet.isLocked()) { return jpaDistributionSet; } else { - if (!jpaDistributionSet.isComplete()) { + if (!distributionSet.isComplete()) { throw new IncompleteDistributionSetException("Could not be locked while incomplete!"); } lockSoftwareModules(jpaDistributionSet); @@ -298,16 +348,13 @@ public class JpaDistributionSetManagement @Override public JpaDistributionSet getValidAndComplete(final long id) { final JpaDistributionSet distributionSet = getValid0(id); - if (!distributionSet.isComplete()) { throw new IncompleteDistributionSetException( "Distribution set of type " + distributionSet.getType().getKey() + " is incomplete: " + distributionSet.getId()); } - if (distributionSet.isDeleted()) { throw new DeletedException(DistributionSet.class, id); } - return distributionSet; } @@ -357,6 +404,24 @@ public class JpaDistributionSetManagement QuotaHelper.assertAssignmentQuota(requested, maxMetaData, String.class, DistributionSet.class); } + private static boolean completeComparison(final AtomicReference completeComparison) { + final Node.Comparison comparison = completeComparison.get(); + if (comparison.getOp() == Node.Comparison.Operator.EQ) { + return Boolean.parseBoolean(String.valueOf(comparison.getValue())); + } else if (comparison.getOp() == Node.Comparison.Operator.NE) { + return !Boolean.parseBoolean(String.valueOf(comparison.getValue())); + } else { + throw new RSQLParameterSyntaxException("Unsupported operator for 'complete': " + comparison.getOp()); + } + } + + private static Page filter(final Page page, final boolean completed) { + final List filtered = page.getContent().stream() + .filter(ds -> ds.isComplete() == completed) + .toList(); + return new PageImpl<>(filtered, page.getPageable(), page.getTotalElements()); + } + private static Collection notFound(final Collection distributionSetIds, final List foundDistributionSets) { final Map foundDistributionSetMap = foundDistributionSets.stream() .collect(Collectors.toMap(JpaDistributionSet::getId, Function.identity())); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaSoftwareModuleManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaSoftwareModuleManagement.java index 5d1686e31..151e50ee2 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaSoftwareModuleManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaSoftwareModuleManagement.java @@ -28,6 +28,7 @@ import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.SoftwareModuleFields; import org.eclipse.hawkbit.repository.SoftwareModuleManagement; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.exception.IncompleteSoftwareModuleException; import org.eclipse.hawkbit.repository.exception.LockedException; import org.eclipse.hawkbit.repository.jpa.JpaManagementHelper; import org.eclipse.hawkbit.repository.jpa.acm.AccessController; @@ -142,6 +143,9 @@ public class JpaSoftwareModuleManagement extends if (jpaSoftwareModule.isLocked()) { return jpaSoftwareModule; } else { + if (!softwareModule.isComplete()) { + throw new IncompleteSoftwareModuleException("Could not be locked while incomplete!"); + } jpaSoftwareModule.lock(); return jpaRepository.save(jpaSoftwareModule); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaDistributionSet.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaDistributionSet.java index b42a61361..fe8a6d69e 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaDistributionSet.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaDistributionSet.java @@ -13,6 +13,7 @@ import java.io.Serial; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -45,13 +46,13 @@ import org.eclipse.hawkbit.repository.event.remote.DistributionSetDeletedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetUpdatedEvent; import org.eclipse.hawkbit.repository.exception.DistributionSetTypeUndefinedException; -import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; import org.eclipse.hawkbit.repository.exception.LockedException; import org.eclipse.hawkbit.repository.exception.UnsupportedSoftwareModuleForThisDistributionSetException; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetTag; import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; import org.springframework.context.ApplicationEvent; import org.springframework.core.annotation.Order; @@ -122,9 +123,6 @@ public class JpaDistributionSet @Column(name = "meta_value", length = DistributionSet.METADATA_MAX_VALUE_SIZE) private Map metadata = new HashMap<>(); - @Column(name = "complete") - private boolean complete; - @Setter @Column(name = "locked") private boolean locked; @@ -161,6 +159,20 @@ public class JpaDistributionSet return this; } + @Override + public boolean isComplete() { + return Optional.ofNullable(type).map(dsType -> { + if (getModules().stream().anyMatch(module -> !module.isComplete())) { + return false; // incomplete module + } + final List smTypes = getModules().stream() + .map(SoftwareModule::getType) + .distinct() + .toList(); + return !smTypes.isEmpty() && new HashSet<>(smTypes).containsAll(dsType.getMandatoryModuleTypes()); + }).orElse(true); + } + public void addModule(final SoftwareModule softwareModule) { if (isLocked()) { throw new LockedException(JpaDistributionSet.class, getId(), "ADD_SOFTWARE_MODULE"); @@ -180,9 +192,7 @@ public class JpaDistributionSet .findAny().ifPresent(modules::remove); } - if (modules.add(softwareModule)) { - complete = type.checkComplete(this); - } + modules.add(softwareModule); } public void removeModule(final SoftwareModule softwareModule) { @@ -190,8 +200,8 @@ public class JpaDistributionSet throw new LockedException(JpaDistributionSet.class, getId(), "REMOVE_SOFTWARE_MODULE"); } - if (modules != null && modules.removeIf(m -> m.getId().equals(softwareModule.getId()))) { - complete = type.checkComplete(this); + if (modules != null) { + modules.removeIf(m -> m.getId().equals(softwareModule.getId())); } } @@ -199,20 +209,17 @@ public class JpaDistributionSet return Collections.unmodifiableSet(tags); } - public boolean addTag(final DistributionSetTag tag) { + public void addTag(final DistributionSetTag tag) { if (tags == null) { tags = new HashSet<>(); } - - return tags.add(tag); + tags.add(tag); } - public boolean removeTag(final DistributionSetTag tag) { - if (tags == null) { - return false; + public void removeTag(final DistributionSetTag tag) { + if (tags != null) { + tags.remove(tag); } - - return tags.remove(tag); } public void invalidate() { @@ -226,7 +233,7 @@ public class JpaDistributionSet @Override public void fireUpdateEvent() { - publishEventWithEventPublisher(new DistributionSetUpdatedEvent(this, complete)); + publishEventWithEventPublisher(new DistributionSetUpdatedEvent(this)); if (deleted) { publishEventWithEventPublisher(new DistributionSetDeletedEvent(getTenant(), getId(), getClass())); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaDistributionSetType.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaDistributionSetType.java index da46d3e20..dad0423ad 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaDistributionSetType.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaDistributionSetType.java @@ -112,15 +112,6 @@ public class JpaDistributionSetType extends AbstractJpaTypeEntity implements Dis return this; } - @Override - public boolean checkComplete(final DistributionSet distributionSet) { - final List smTypes = distributionSet.getModules().stream() - .map(SoftwareModule::getType) - .distinct() - .toList(); - return !smTypes.isEmpty() && new HashSet<>(smTypes).containsAll(getMandatoryModuleTypes()); - } - @Override public String toString() { return "DistributionSetType [key=" + getKey() + ", isDeleted()=" + isDeleted() + ", getId()=" + getId() + "]"; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaSoftwareModule.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaSoftwareModule.java index 33d409378..1e5552274 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaSoftwareModule.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaSoftwareModule.java @@ -15,6 +15,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import jakarta.persistence.CascadeType; import jakarta.persistence.CollectionTable; @@ -146,6 +147,17 @@ public class JpaSoftwareModule } } + @Override + public boolean isComplete() { + return Optional.ofNullable(type).map(smType -> { + if (smType.getMinArtifacts() > 0) { + return getArtifacts().size() >= smType.getMinArtifacts(); + } else { + return true; + } + }).orElse(true); + } + public void removeArtifact(final Artifact artifact) { if (isLocked()) { throw new LockedException(JpaSoftwareModule.class, getId(), "REMOVE_ARTIFACT"); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaSoftwareModuleType.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaSoftwareModuleType.java index e8ea3ca50..43796cd25 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaSoftwareModuleType.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaSoftwareModuleType.java @@ -45,6 +45,11 @@ public class JpaSoftwareModuleType extends AbstractJpaTypeEntity implements Soft @Serial private static final long serialVersionUID = 1L; + @Setter(value = lombok.AccessLevel.PRIVATE) // used via reflection + @Column(name = "min_artifacts", nullable = false) + @Min(0) + private int minArtifacts; + @Setter(value = lombok.AccessLevel.PRIVATE) // used via reflection @Column(name = "max_ds_assignments", nullable = false) @Min(1) diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/DistributionSetSpecification.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/DistributionSetSpecification.java index a0f2cb936..ef47e85ef 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/DistributionSetSpecification.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/DistributionSetSpecification.java @@ -57,26 +57,6 @@ public final class DistributionSetSpecification { return (dsRoot, query, cb) -> cb.equal(dsRoot.get(JpaDistributionSet_.deleted), isDeleted); } - /** - * {@link Specification} for retrieving {@link DistributionSet}s by its COMPLETED attribute. - * - * @param isCompleted TRUE/FALSE are compared to the attribute COMPLETED. If NULL the attribute is ignored - * @return the {@link DistributionSet} {@link Specification} - */ - public static Specification isCompleted(final Boolean isCompleted) { - return (dsRoot, query, cb) -> cb.equal(dsRoot.get(JpaDistributionSet_.complete), isCompleted); - } - - /** - * {@link Specification} for retrieving {@link DistributionSet}s by its VALID attribute. - * - * @param isValid TRUE/FALSE are compared to the attribute VALID. If NULL the attribute is ignored - * @return the {@link DistributionSet} {@link Specification} - */ - public static Specification isValid(final Boolean isValid) { - return (dsRoot, query, cb) -> cb.equal(dsRoot.get(JpaDistributionSet_.valid), isValid); - } - /** * {@link Specification} for retrieving {@link DistributionSet} with given {@link DistributionSet#getId()}s. * diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/DistributionSetUpdatedEventTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/DistributionSetUpdatedEventTest.java index dc89cceaa..5034b178e 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/DistributionSetUpdatedEventTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/DistributionSetUpdatedEventTest.java @@ -31,7 +31,7 @@ class DistributionSetUpdatedEventTest extends AbstractRemoteEntityEventTest createRemoteEvent(final DistributionSet baseEntity, final Class> eventType) { - return new DistributionSetUpdatedEvent(baseEntity, true); + return new DistributionSetUpdatedEvent(baseEntity); } @Override diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DeploymentManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DeploymentManagementTest.java index d8ad31423..8b5f20c75 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DeploymentManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DeploymentManagementTest.java @@ -1357,8 +1357,7 @@ class DeploymentManagementTest extends AbstractJpaIntegrationTest { List allFoundDS = distributionSetManagement.findAll(PAGE).getContent(); assertThat(allFoundDS).as("no ds should be founded").isEmpty(); - assertThat(distributionSetRepository.findAll(JpaManagementHelper.combineWithAnd( - List.of(DistributionSetSpecification.isDeleted(true), DistributionSetSpecification.isCompleted(true))), PAGE).getContent()) + assertThat(distributionSetRepository.findAll(DistributionSetSpecification.isDeleted(true), PAGE).getContent()) .as("wrong size of founded ds").hasSize(noOfDistributionSets); IntStream.range(0, deploymentResult.getDistributionSets().size()).forEach(i -> testdataFactory.sendUpdateActionStatusToTargets( @@ -1369,9 +1368,8 @@ class DeploymentManagementTest extends AbstractJpaIntegrationTest { // verify that the result is the same, even though distributionSet dsA has been installed // successfully and no activeAction is referring to created distribution sets allFoundDS = distributionSetManagement.findAll(pageRequest).getContent(); - assertThat(allFoundDS).as("no ds should be founded").isEmpty(); - assertThat(distributionSetRepository.findAll(JpaManagementHelper.combineWithAnd( - List.of(DistributionSetSpecification.isDeleted(true), DistributionSetSpecification.isCompleted(true))), PAGE).getContent()) + assertThat(allFoundDS).as("no ds should be found").isEmpty(); + assertThat(distributionSetRepository.findAll(DistributionSetSpecification.isDeleted(true), PAGE).getContent()) .as("wrong size of founded ds").hasSize(noOfDistributionSets); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetManagementTest.java index f102e8517..2c817cdb3 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetManagementTest.java @@ -12,14 +12,15 @@ package org.eclipse.hawkbit.repository.jpa.management; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.io.ByteArrayInputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import jakarta.validation.ConstraintViolationException; @@ -33,6 +34,7 @@ import org.eclipse.hawkbit.repository.DistributionSetTypeManagement; import org.eclipse.hawkbit.repository.Identifiable; import org.eclipse.hawkbit.repository.RepositoryProperties; import org.eclipse.hawkbit.repository.SoftwareModuleManagement; +import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement; import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetTagCreatedEvent; @@ -49,13 +51,16 @@ import org.eclipse.hawkbit.repository.exception.UnsupportedSoftwareModuleForThis import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.ActionCancellationType; +import org.eclipse.hawkbit.repository.model.ArtifactUpload; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation; import org.eclipse.hawkbit.repository.model.DistributionSetTag; +import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.NamedEntity; import org.eclipse.hawkbit.repository.model.NamedVersionedEntity; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; import org.eclipse.hawkbit.repository.model.Statistic; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.test.matcher.Expect; @@ -688,6 +693,95 @@ class DistributionSetManagementTest extends AbstractRepositoryManagementWithMeta .isThrownBy(() -> distributionSetManagement.getValidAndComplete(distributionSetId)); } + /** + * Verifies that when no SoftwareModules are assigned to a Distribution then the DistributionSet is not complete. + */ + @Test + void incompleteIfNoSoftwareModulesAssigned() { + final SoftwareModuleType softwareModuleType = softwareModuleTypeManagement + .create(SoftwareModuleTypeManagement.Create.builder().key("newType").name("new Type").build()); + + final DistributionSetType distributionSetType = distributionSetTypeManagement + .create(DistributionSetTypeManagement.Create.builder() + .key("newType").name("new Type").optionalModuleTypes(Set.of(softwareModuleType)).build()); + final DistributionSet distributionSetIncomplete = testdataFactory.createDistributionSet( + "DistributionOne", "3.1.2", distributionSetType, new ArrayList<>()); + assertThat(distributionSetIncomplete.isComplete()).isFalse(); + assertThatExceptionOfType(IncompleteDistributionSetException.class) + .isThrownBy(() -> distributionSetManagement.lock(distributionSetIncomplete)); + final long dsId = distributionSetIncomplete.getId(); + assertThatExceptionOfType(IncompleteDistributionSetException.class) + .isThrownBy(() -> distributionSetManagement.getValidAndComplete(dsId)); + + final SoftwareModule softwareModule = softwareModuleManagement + .create(SoftwareModuleManagement.Create.builder().name("ds").version("1.0.0").type(softwareModuleType).build()); + assertThat(softwareModule.isComplete()).isTrue(); + + distributionSetManagement.assignSoftwareModules(distributionSetIncomplete.getId(), List.of(softwareModule.getId())); + + final DistributionSet distributionSetComplete = distributionSetManagement.get(distributionSetIncomplete.getId()); + assertThat(distributionSetComplete.isComplete()).isTrue(); + assertThatNoException().isThrownBy(() -> distributionSetManagement.lock(distributionSetComplete)); + assertThat(softwareModuleManagement.get(softwareModule.getId()).isComplete()).isTrue(); + } + + @Test + void incompleteIfNoSoftwareModuleOfMandatorySoftwareModuleTypeAssigned() { + final SoftwareModuleType softwareModuleType = softwareModuleTypeManagement + .create(SoftwareModuleTypeManagement.Create.builder().key("newType").name("new Type").build()); + final DistributionSetType distributionSetType = distributionSetTypeManagement + .create(DistributionSetTypeManagement.Create.builder() + .key("newType").name("new Type").mandatoryModuleTypes(Set.of(softwareModuleType)).build()); + final DistributionSet distributionSetIncomplete = testdataFactory.createDistributionSet( + "DistributionOne", "3.1.2", distributionSetType, new ArrayList<>()); + assertThat(distributionSetIncomplete.isComplete()).isFalse(); + assertThatExceptionOfType(IncompleteDistributionSetException.class) + .isThrownBy(() -> distributionSetManagement.lock(distributionSetIncomplete)); + final long dsId = distributionSetIncomplete.getId(); + assertThatExceptionOfType(IncompleteDistributionSetException.class) + .isThrownBy(() -> distributionSetManagement.getValidAndComplete(dsId)); + + final SoftwareModule softwareModule = softwareModuleManagement + .create(SoftwareModuleManagement.Create.builder().name("ds").version("1.0.0").type(softwareModuleType).build()); + assertThat(softwareModule.isComplete()).isTrue(); + + distributionSetManagement.assignSoftwareModules(distributionSetIncomplete.getId(), List.of(softwareModule.getId())); + + final DistributionSet distributionSetComplete = distributionSetManagement.get(distributionSetIncomplete.getId()); + assertThat(distributionSetComplete.isComplete()).isTrue(); + assertThatNoException().isThrownBy(() -> distributionSetManagement.lock(distributionSetComplete)); + assertThat(softwareModuleManagement.get(softwareModule.getId()).isComplete()).isTrue(); + } + + @Test + void incompleteIfDistributionSetSoftwareModuleIsIncomplete() { + final SoftwareModuleType softwareModuleType = softwareModuleTypeManagement + .create(SoftwareModuleTypeManagement.Create.builder().key("newType").name("new Type").minArtifacts(1).build()); + final SoftwareModule softwareModuleIncomplete = softwareModuleManagement + .create(SoftwareModuleManagement.Create.builder().name("ds").version("1.0.0").type(softwareModuleType).build()); + assertThat(softwareModuleIncomplete.isComplete()).isFalse(); + + final DistributionSetType distributionSetType = distributionSetTypeManagement + .create(DistributionSetTypeManagement.Create.builder() + .key("newType").name("new Type").optionalModuleTypes(Set.of(softwareModuleType)).build()); + final DistributionSet distributionSetIncomplete = testdataFactory.createDistributionSet( + "DistributionOne", "3.1.2", distributionSetType, new ArrayList<>()); + assertThat(distributionSetIncomplete.isComplete()).isFalse(); // no software modules assigned yet + distributionSetManagement.assignSoftwareModules(distributionSetIncomplete.getId(), List.of(softwareModuleIncomplete.getId())); + assertThat(distributionSetIncomplete.isComplete()).isFalse(); // has software module assigned, but incomplete + + // add artifact - so it should become complete + artifactManagement.create(new ArtifactUpload( + new ByteArrayInputStream(randomBytes(10)), null, 10, null, + softwareModuleIncomplete.getId(), "file1", false)); + assertThat(softwareModuleManagement.get(softwareModuleIncomplete.getId()).isComplete()).isTrue(); + + final DistributionSet distributionSetComplete = distributionSetManagement.get(distributionSetIncomplete.getId()); + assertThat(distributionSetComplete.isComplete()).isTrue(); + assertThatNoException().isThrownBy(() -> distributionSetManagement.lock(distributionSetComplete)); + assertThat(softwareModuleManagement.get(softwareModuleIncomplete.getId()).isComplete()).isTrue(); + } + /** * Get the Rollouts count by status statistics for a specific Distribution Set */ diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetTypeManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetTypeManagementTest.java index 056806eab..fce1a6f61 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetTypeManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetTypeManagementTest.java @@ -108,18 +108,6 @@ class DistributionSetTypeManagementTest extends AbstractRepositoryManagementTest assertThat(distributionSetTypeManagement.count()).isEqualTo(existing); } - /** - * Verifies that when no SoftwareModules are assigned to a Distribution then the DistributionSet is not complete. - */ - @Test - void incompleteIfDistributionSetHasNoSoftwareModulesAssigned() { - final JpaDistributionSetType jpaDistributionSetType = (JpaDistributionSetType) distributionSetTypeManagement - .create(Create.builder().key("newType").name("new Type").build()); - final DistributionSet distributionSet = testdataFactory.createDistributionSet( - "DistributionOne", "3.1.2", jpaDistributionSetType, new ArrayList<>()); - assertThat(jpaDistributionSetType.checkComplete(distributionSet)).isFalse(); - } - /** * Verifies that the quota for software module types per distribution set type is enforced as expected. */ diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/SoftwareModuleManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/SoftwareModuleManagementTest.java index f95a21714..cffa35690 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/SoftwareModuleManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/SoftwareModuleManagementTest.java @@ -11,6 +11,7 @@ package org.eclipse.hawkbit.repository.jpa.management; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; @@ -25,9 +26,12 @@ import jakarta.validation.ConstraintViolationException; import org.eclipse.hawkbit.artifact.exception.ArtifactBinaryNotFoundException; import org.eclipse.hawkbit.repository.DistributionSetManagement; +import org.eclipse.hawkbit.repository.SoftwareModuleManagement; import org.eclipse.hawkbit.repository.SoftwareModuleManagement.Create; import org.eclipse.hawkbit.repository.SoftwareModuleManagement.Update; +import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement; import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent; +import org.eclipse.hawkbit.repository.exception.IncompleteSoftwareModuleException; import org.eclipse.hawkbit.repository.exception.LockedException; import org.eclipse.hawkbit.repository.jpa.RandomGeneratedInputStream; import org.eclipse.hawkbit.repository.model.Artifact; @@ -419,6 +423,30 @@ class SoftwareModuleManagementTest verifyThrownExceptionBy(() -> softwareModuleManagement.update(Update.builder().id(NOT_EXIST_IDL).build()), "SoftwareModule"); } + /** + * Verifies that when no SoftwareModules are assigned to a Distribution then the DistributionSet is not complete. + */ + @Test + void incompleteIfSoftwareModule() { + final SoftwareModuleType softwareModuleType = softwareModuleTypeManagement + .create(SoftwareModuleTypeManagement.Create.builder().key("newType").name("new Type").minArtifacts(1).build()); + final SoftwareModule softwareModuleIncomplete = softwareModuleManagement + .create(SoftwareModuleManagement.Create.builder().name("ds").version("1.0.0").type(softwareModuleType).build()); + assertThat(softwareModuleIncomplete.isComplete()).isFalse(); + assertThatExceptionOfType(IncompleteSoftwareModuleException.class) + .isThrownBy(() -> softwareModuleManagement.lock(softwareModuleIncomplete)); + + // add artifact - so it should become complete + artifactManagement.create(new ArtifactUpload( + new ByteArrayInputStream(randomBytes(10)), null, 10, null, + softwareModuleIncomplete.getId(), "file1", false)); + + final SoftwareModule softwareModuleComplete = softwareModuleManagement.get(softwareModuleIncomplete.getId()); + assertThat(softwareModuleComplete.isComplete()).isTrue(); + assertThatNoException().isThrownBy(() -> softwareModuleManagement.lock(softwareModuleComplete)); + assertThat(softwareModuleManagement.get(softwareModuleIncomplete.getId()).isComplete()).isTrue(); + } + private SoftwareModule createSoftwareModuleWithArtifacts( final SoftwareModuleType type, final String name, final String version, final int numberArtifacts) { final long countSoftwareModule = softwareModuleRepository.count();